diff --git a/.browserslistrc b/.browserslistrc index 6784945a5..427441dc9 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -8,4 +8,10 @@ # You can see what browsers were selected by your queries by running: # npx browserslist -defaults \ No newline at end of file +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/.editorconfig b/.editorconfig index c82009e40..faf506c4e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,6 @@ # Editor configuration, see https://editorconfig.org root = true - [*] charset = utf-8 indent_style = space @@ -20,10 +19,3 @@ trim_trailing_whitespace = false [*.yml] indent_size = 2 - -[*.csproj] -indent_size = 2 - -[*.cs] -# Disable SonarLint warning S1075 (Don't use hardcoded url) -dotnet_diagnostic.S1075.severity = none diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml deleted file mode 100644 index 845d3e3f3..000000000 --- a/.github/DISCUSSION_TEMPLATE/ideas.yml +++ /dev/null @@ -1,49 +0,0 @@ -title: "[Kavita] Idea / Feature Submission" -labels: - - "Idea Submission" -body: - - type: markdown - attributes: - value: | - ## Idea Submission for Kavita 💡 - - Please fill out the details below, and let's make Kavita even better together! - - - type: textarea - id: idea-description - attributes: - label: Idea Description - value: | - Go into as much detail as possible to explain why your idea should be added to Kavita. Try to present some use cases and examples of how it would help other users. The more detail you have the better. - - - type: dropdown - id: idea-category - attributes: - label: Idea Category - options: - - API - - Feature Enhancement - - User Experience - - Performance Improvement - - Web UI - description: "What area would your idea help with?" - validations: - required: true - - - type: input - id: duration-of-use - attributes: - label: Duration of Using Kavita - description: "How long have you been using Kavita?" - - - type: checkboxes - attributes: - label: Before submitting - options: - - label: "I've already searched for existing ideas before posting." - required: true - - - type: markdown - attributes: - value: | - ### Thank you for contributing to Kavita's future! 🚀 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..bfd2bab0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: needs-triage +assignees: '' + +--- + +**If this is a feature request, request [here](https://feats.kavitareader.com/) instead. Feature requests will be deleted from Github.** + +Please put as much information as possible to help me understand your issue. OS, browser, version are very important! + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS, Docker] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] (can be found on Server Settings -> System tab) + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 805c3b61d..02cbdf152 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,17 +1,24 @@ name: Bug Report -description: Help us make Kavita better for everyone by submitting issues you run into while using the program. -title: "Put a short summary of what went wrong here" +description: Create a report to help us improve +title: "" labels: ["needs-triage"] +assignees: body: - type: markdown attributes: - value: "Thanks for taking the time to fill out this bug report!" + value: | + Thanks for taking the time to fill out this bug report! + - type: markdown + attributes: + value: | + If you have a feature request, please go to our [Feature Requests](https://feats.kavitareader.com) page. - type: textarea id: what-happened attributes: label: What happened? - description: Don't forget to tell us what steps you took so we can try to reproduce. + description: Also tell us, what steps you took so we can try to reproduce. placeholder: Tell us what you see! + value: "" validations: required: true - type: textarea @@ -19,35 +26,33 @@ body: attributes: label: What did you expect? description: What did you expect to happen? - placeholder: Tell us what you expected to see! Go in as much detail as possible so we can confirm if the behavior is something that is broken. + placeholder: Tell us what you expected to see! + value: "" validations: required: true - - type: dropdown + - type: textarea id: version attributes: - label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists. - multiple: false - options: - - 0.8.7 - Stable - - Nightly Testing Branch + label: Version + description: What version of our software are you running? + placeholder: Can be found by going to Server Settings > System + value: "" validations: required: true - type: dropdown id: OS attributes: - label: What operating system is Kavita being hosted from? + label: What OS is Kavita being run on? multiple: false options: - - Docker (LSIO Container) - - Docker (Dockerhub Container) - - Docker (Other) + - Docker - Windows - Linux - Mac - type: dropdown id: desktop-OS attributes: - label: If the issue is being seen on Desktop, what OS are you running where you see the issue? + label: If issue being seen on Desktop, what OS are you running where you see the issue? multiple: false options: - Windows @@ -56,18 +61,17 @@ body: - type: dropdown id: desktop-browsers attributes: - label: If the issue is being seen in the UI, what browsers are you seeing the problem on? + label: If issue being seen on Desktop, what browsers are you seeing the problem on? multiple: true options: - Firefox - Chrome - Safari - Microsoft Edge - - Other (List in "Additional Notes" box) - type: dropdown id: mobile-OS attributes: - label: If the issue is being seen on Mobile, what OS are you running where you see the issue? + label: If issue being seen on Mobile, what OS are you running where you see the issue? multiple: false options: - Android @@ -75,13 +79,13 @@ body: - type: dropdown id: mobile-browsers attributes: - label: If the issue is being seen on the Mobile UI, what browsers are you seeing the problem on? + label: If issue being seen on Mobile, what browsers are you seeing the problem on? multiple: true options: - Firefox - Chrome - Safari - - Other iOS Browser + - Microsoft Edge - type: textarea id: logs attributes: @@ -93,4 +97,7 @@ body: attributes: label: Additional Notes description: Any other information about the issue not covered in this form? - placeholder: e.g. Running Kavita on a Raspberry Pi, updating from X version, using LSIO container, etc + placeholder: e.g. Running Kavita on a raspberry pi + value: "" + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e9be08116..ec4bb386b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1 @@ -blank_issues_enabled: false -contact_links: - - name: Feature Requests - url: https://github.com/Kareadita/Kavita/discussions - about: Suggest an idea for the Kavita project +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 044864734..395c6edb6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -10,26 +10,64 @@ jobs: runs-on: windows-latest steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI shell: powershell - run: dotnet tool install -g Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - name: Install dependencies run: dotnet restore - - uses: actions/upload-artifact@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v3 + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + shell: powershell + run: | + New-Item -Path .\.sonar\scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + + - name: Sonar Scan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: | + .\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + dotnet build --configuration Release + .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + - name: Test run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/canary-workflow.yml b/.github/workflows/canary-workflow.yml index b919030b0..af353b915 100644 --- a/.github/workflows/canary-workflow.yml +++ b/.github/workflows/canary-workflow.yml @@ -9,14 +9,14 @@ on: jobs: build: name: Upload Kavita.Common for Version Bump - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj @@ -24,16 +24,16 @@ jobs: version: name: Bump version needs: [ build ] - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Bump versions uses: SiqiLu/dotnet-bump-version@2.0.0 @@ -45,7 +45,7 @@ jobs: canary: name: Build Canary Docker needs: [ build, version ] - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: packages: write contents: read @@ -59,14 +59,14 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: ref: canary - name: NodeJS to Compile WebUI - uses: actions/setup-node@v4 + uses: actions/setup-node@v3 with: - node-version: 20 + node-version: '16' - run: | cd UI/Web || exit echo 'Installing web dependencies' @@ -81,7 +81,7 @@ jobs: cd ../ || exit - name: Get csproj Version - uses: kzrnm/get-net-sdk-project-versions-action@v2 + uses: kzrnm/get-net-sdk-project-versions-action@v1 id: get-version with: proj-path: Kavita.Common/Kavita.Common.csproj @@ -96,38 +96,38 @@ jobs: run: echo "${{steps.get-version.outputs.assembly-version}}" - name: Compile dotnet app - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - run: ./monorepo-build.sh - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v2 - name: Build and push id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 7ce4276bc..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,87 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "develop"] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "develop" ] - schedule: - - cron: '33 12 * * 5' - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp', 'javascript-typescript' ] - # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] - # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - - run: | - echo "Run, Build Application using script" - dotnet build Kavita.sln - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/develop-workflow.yml b/.github/workflows/develop-workflow.yml index 006127645..86ae6eb37 100644 --- a/.github/workflows/develop-workflow.yml +++ b/.github/workflows/develop-workflow.yml @@ -2,30 +2,23 @@ name: Nightly Workflow on: push: + branches: ['!release/**'] + pull_request: branches: [ 'develop', '!release/**' ] - workflow_dispatch: + types: [ closed ] jobs: - debug: - runs-on: ubuntu-24.04 - steps: - - name: Debug Info - run: | - echo "Event Name: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" - echo "Not Contains Release: ${{ !contains(github.head_ref, 'release') }}" - echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}" build: name: Upload Kavita.Common for Version Bump - runs-on: ubuntu-24.04 - if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/develop') }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj @@ -33,20 +26,20 @@ jobs: version: name: Bump version needs: [ build ] - runs-on: ubuntu-24.04 - if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/develop') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Bump versions - uses: majora2007/dotnet-bump-version@v0.0.10 + uses: SiqiLu/dotnet-bump-version@2.0.0 with: version_files: Kavita.Common/Kavita.Common.csproj github_token: ${{ secrets.REPO_GHA_PAT }} @@ -55,8 +48,8 @@ jobs: develop: name: Build Nightly Docker needs: [ build, version ] - runs-on: ubuntu-24.04 - if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/develop') }} permissions: packages: write contents: read @@ -89,18 +82,18 @@ jobs: echo "BODY=$body" >> $GITHUB_OUTPUT - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: ref: develop - name: NodeJS to Compile WebUI - uses: actions/setup-node@v4 + uses: actions/setup-node@v3 with: - node-version: 20 + node-version: '16' - run: | cd UI/Web || exit echo 'Installing web dependencies' - npm ci + npm install --legacy-peer-deps echo 'Building UI' npm run prod @@ -111,7 +104,7 @@ jobs: cd ../ || exit - name: Get csproj Version - uses: kzrnm/get-net-sdk-project-versions-action@v2 + uses: kzrnm/get-net-sdk-project-versions-action@v1 id: get-version with: proj-path: Kavita.Common/Kavita.Common.csproj @@ -126,63 +119,49 @@ jobs: run: echo "${{steps.get-version.outputs.assembly-version}}" - name: Compile dotnet app - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - run: ./monorepo-build.sh - name: Login to Docker Hub - uses: docker/login-action@v3 - if: ${{ github.repository_owner == 'Kareadita' }} + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata (tags, labels) for Docker - id: docker_meta_nightly - uses: docker/metadata-action@v5 - with: - tags: | - type=raw,value=nightly - type=raw,value=nightly-${{ steps.parse-version.outputs.VERSION }} - images: | - name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} - name=ghcr.io/${{ github.repository }} + uses: docker/setup-buildx-action@v2 - name: Build and push id: docker_build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: ${{ steps.docker_meta_nightly.outputs.tags }} - labels: ${{ steps.docker_meta_nightly.outputs.labels }} + tags: jvmilazz0/kavita:nightly, jvmilazz0/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:nightly, ghcr.io/kareadita/kavita:nightly-${{ steps.parse-version.outputs.VERSION }} - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} - name: Notify Discord uses: rjstone/discord-webhook-notify@v1 - if: ${{ github.repository_owner == 'Kareadita' }} with: severity: info description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }} diff --git a/.github/workflows/openapi-gen.yml b/.github/workflows/openapi-gen.yml deleted file mode 100644 index 45446d045..000000000 --- a/.github/workflows/openapi-gen.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Generate OpenAPI Documentation - -on: - push: - branches: [ 'develop', '!release/**' ] - paths: - - '**/*.cs' - - '**/*.csproj' - pull_request: - branches: [ 'develop', '!release/**' ] - workflow_dispatch: - -jobs: - generate-openapi: - runs-on: ubuntu-latest - # Only run on direct pushes to develop, not PRs - if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'Kareadita' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build project - run: dotnet build API/API.csproj --configuration Debug - - - name: Get Swashbuckle version - id: swashbuckle-version - run: | - VERSION=$(grep -o '> $GITHUB_OUTPUT - echo "Found Swashbuckle.AspNetCore version: $VERSION" - - - name: Install matching Swashbuckle CLI tool - run: | - dotnet new tool-manifest --force - dotnet tool install Swashbuckle.AspNetCore.Cli --version ${{ steps.swashbuckle-version.outputs.VERSION }} - - - name: Generate OpenAPI file - run: dotnet swagger tofile --output openapi.json API/bin/Debug/net9.0/API.dll v1 - - - name: Check for changes - id: git-check - run: | - git add openapi.json - git diff --staged --quiet openapi.json || echo "has_changes=true" >> $GITHUB_OUTPUT - - - name: Commit and push if changed - if: steps.git-check.outputs.has_changes == 'true' - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - git commit -m "Update OpenAPI documentation" openapi.json - - # Pull latest changes with rebase to avoid merge commits - git pull --rebase origin develop - - git push - env: - GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 51589221f..7482deb0b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -1,13 +1,15 @@ name: Validate PR Body on: + push: + branches: '**' pull_request: branches: [ main, develop, canary ] types: [synchronize] jobs: check_pr: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: Extract branch name shell: bash diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 757ce1075..3555b278f 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -6,35 +6,25 @@ on: pull_request: branches: [ 'develop' ] types: [ closed ] - workflow_dispatch: jobs: - debug: - runs-on: ubuntu-24.04 - steps: - - name: Debug Info - run: | - echo "Event Name: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" - echo "Not Contains Release: ${{ !contains(github.head_ref, 'release') }}" - echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}" if_merged: if: github.event.pull_request.merged == true && contains(github.head_ref, 'release') - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - run: | echo The PR was merged build: name: Upload Kavita.Common for Version Bump - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest if: github.event.pull_request.merged == true && contains(github.head_ref, 'release') steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj @@ -43,7 +33,7 @@ jobs: name: Build Stable and Nightly Docker if Release needs: [ build ] if: github.event.pull_request.merged == true && contains(github.head_ref, 'release') - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: packages: write contents: read @@ -58,25 +48,38 @@ jobs: - name: Parse PR body id: parse-body run: | - body="Read full changelog: https://github.com/Kareadita/Kavita/releases/latest" + body="${{ steps.findPr.outputs.body }}" + if [[ ${#body} -gt 1870 ]] ; then + body=${body:0:1870} + body="${body}...and much more. + Read full changelog: https://github.com/Kareadita/Kavita/releases/latest" + fi + + body=${body//\'/} + body=${body//'%'/'%25'} + body=${body//$'\n'/'%0A'} + body=${body//$'\r'/'%0D'} + body=${body//$'`'/'%60'} + body=${body//$'>'/'%3E'} echo $body echo "BODY=$body" >> $GITHUB_OUTPUT + - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: ref: develop - name: NodeJS to Compile WebUI - uses: actions/setup-node@v4 + uses: actions/setup-node@v3 with: - node-version: 20 + node-version: '16' - run: | cd UI/Web || exit echo 'Installing web dependencies' - npm ci + npm install --legacy-peer-deps echo 'Building UI' npm run prod @@ -87,7 +90,7 @@ jobs: cd ../ || exit - name: Get csproj Version - uses: kzrnm/get-net-sdk-project-versions-action@v2 + uses: kzrnm/get-net-sdk-project-versions-action@v1 id: get-version with: proj-path: Kavita.Common/Kavita.Common.csproj @@ -104,79 +107,72 @@ jobs: id: parse-version - name: Compile dotnet app - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - run: ./monorepo-build.sh - name: Login to Docker Hub - uses: docker/login-action@v3 - if: ${{ github.repository_owner == 'Kareadita' }} + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata (tags, labels) for Docker - id: docker_meta_stable - uses: docker/metadata-action@v5 - with: - tags: | - type=raw,value=latest - type=raw,value=${{ steps.parse-version.outputs.VERSION }} - images: | - name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} - name=ghcr.io/${{ github.repository }} + uses: docker/setup-buildx-action@v2 - name: Build and push stable id: docker_build_stable - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: ${{ steps.docker_meta_stable.outputs.tags }} - labels: ${{ steps.docker_meta_stable.outputs.labels }} - - - name: Extract metadata (tags, labels) for Docker - id: docker_meta_nightly - uses: docker/metadata-action@v5 - with: - tags: | - type=raw,value=nightly - type=raw,value=nightly-${{ steps.parse-version.outputs.VERSION }} - images: | - name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} - name=ghcr.io/${{ github.repository }} + tags: jvmilazz0/kavita:latest, jvmilazz0/kavita:${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:latest, ghcr.io/kareadita/kavita:${{ steps.parse-version.outputs.VERSION }} - name: Build and push nightly id: docker_build_nightly - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: ${{ steps.docker_meta_nightly.outputs.tags }} - labels: ${{ steps.docker_meta_nightly.outputs.labels }} + tags: jvmilazz0/kavita:nightly, jvmilazz0/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:nightly, ghcr.io/kareadita/kavita:nightly-${{ steps.parse-version.outputs.VERSION }} - name: Image digest run: echo ${{ steps.docker_build_stable.outputs.digest }} - name: Image digest run: echo ${{ steps.docker_build_nightly.outputs.digest }} + + - name: Notify Discord + uses: rjstone/discord-webhook-notify@v1 + with: + severity: info + description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }} + details: '${{ steps.findPr.outputs.body }}' + text: <@&939225192553644133> A new stable build has been released. + webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }} + + - name: Notify Discord + uses: rjstone/discord-webhook-notify@v1 + with: + severity: info + description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }} + details: '${{ steps.findPr.outputs.body }}' + text: <@&939225459156217917> <@&939225350775406643> A new nightly build has been released for docker. + webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }} diff --git a/.gitignore b/.gitignore index 1cffb441d..7da76a034 100644 --- a/.gitignore +++ b/.gitignore @@ -513,7 +513,6 @@ UI/Web/dist/ /API/config/stats/ /API/config/bookmarks/ /API/config/favicons/ -/API/config/cache-long/ /API/config/kavita.db /API/config/kavita.db-shm /API/config/kavita.db-wal @@ -521,26 +520,17 @@ UI/Web/dist/ /API/config/*.db /API/config/*.bak /API/config/*.backup -/API/config/*.csv /API/config/Hangfire.db /API/config/Hangfire-log.db API/config/covers/ -API/config/images/* API/config/stats/* API/config/stats/app_stats.json API/config/pre-metadata/ API/config/post-metadata/ -API/config/*.csv +API/config/relations-imported.csv +API/config/relations.csv API.Tests/TestResults/ UI/Web/.vscode/settings.json /API.Tests/Services/Test Data/ArchiveService/CoverImages/output/* UI/Web/.angular/ BenchmarkDotNet.Artifacts - - -API.Tests/Services/Test Data/ImageService/**/*_output* -API.Tests/Services/Test Data/ImageService/**/*_baseline* -API.Tests/Services/Test Data/ImageService/**/*.html - - -API.Tests/Services/Test Data/ScannerService/ScanTests/**/* diff --git a/.sonarcloud.properties b/.sonarcloud.properties deleted file mode 100644 index 1876ac55a..000000000 --- a/.sonarcloud.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Path to sources -sonar.sources=. -sonar.exclusions=API.Benchmark -#sonar.inclusions= - -# Path to tests -sonar.tests=API.Tests -#sonar.test.exclusions= -#sonar.test.inclusions= - -# Source encoding -sonar.sourceEncoding=UTF-8 - -# Exclusions for copy-paste detection -#sonar.cpd.exclusions= diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index ec9c1884f..61f4f6d7b 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -1,7 +1,7 @@ - net9.0 + net7.0 Exe @@ -10,9 +10,9 @@ - - - + + + @@ -26,10 +26,5 @@ Always - - - PreserveNewest - - diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index ccb44d517..9ef8e237b 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -32,7 +32,7 @@ public class ArchiveServiceBenchmark public ArchiveServiceBenchmark() { _directoryService = new DirectoryService(null, new FileSystem()); - _imageService = new ImageService(null, _directoryService); + _imageService = new ImageService(null, _directoryService, Substitute.For()); _archiveService = new ArchiveService(new NullLogger(), _directoryService, _imageService, Substitute.For()); } diff --git a/API.Benchmark/Data/AesopsFables.epub b/API.Benchmark/Data/AesopsFables.epub deleted file mode 100644 index d2ab9a8b2..000000000 Binary files a/API.Benchmark/Data/AesopsFables.epub and /dev/null differ diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/API.Benchmark/KoreaderHashBenchmark.cs deleted file mode 100644 index c0abfd2ad..000000000 --- a/API.Benchmark/KoreaderHashBenchmark.cs +++ /dev/null @@ -1,41 +0,0 @@ -using API.Helpers.Builders; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; -using System; -using API.Entities.Enums; - -namespace API.Benchmark -{ - [StopOnFirstError] - [MemoryDiagnoser] - [RankColumn] - [Orderer(SummaryOrderPolicy.FastestToSlowest)] - [SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)] - public class KoreaderHashBenchmark - { - private const string sourceEpub = "./Data/AesopsFables.epub"; - - [Benchmark(Baseline = true)] - public void TestBuildManga_baseline() - { - var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) - .Build(); - if (file == null) - { - throw new Exception("Failed to build manga file"); - } - } - - [Benchmark] - public void TestBuildManga_withHash() - { - var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) - .WithHash() - .Build(); - if (file == null) - { - throw new Exception("Failed to build manga file"); - } - } - } -} diff --git a/API.Benchmark/TestBenchmark.cs b/API.Benchmark/TestBenchmark.cs index 511d250aa..0b4880690 100644 --- a/API.Benchmark/TestBenchmark.cs +++ b/API.Benchmark/TestBenchmark.cs @@ -25,7 +25,7 @@ public class TestBenchmark { list.Add(new VolumeDto() { - MinNumber = random.Next(10) > 5 ? 1 : 0, + Number = random.Next(10) > 5 ? 1 : 0, Chapters = GenerateChapters() }); } @@ -49,7 +49,7 @@ public class TestBenchmark private static void SortSpecialChapters(IEnumerable volumes) { - foreach (var v in volumes.WhereNotLooseLeaf()) + foreach (var v in volumes.Where(vDto => vDto.Number == 0)) { v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); } diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index a571a6e72..35b0f80cf 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -1,22 +1,22 @@ - net9.0 + net7.0 false - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -28,18 +28,11 @@ - - + - - - PreserveNewest - - - diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index 9c5f3e726..18f0669cd 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -1,5 +1,6 @@ -using System; +using System.Collections.Generic; using System.Data.Common; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -9,7 +10,6 @@ using API.Helpers; using API.Helpers.Builders; using API.Services; using AutoMapper; -using Hangfire; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -18,34 +18,36 @@ using NSubstitute; namespace API.Tests; -public abstract class AbstractDbTest : AbstractFsTest , IDisposable +public abstract class AbstractDbTest { - protected readonly DataContext Context; - protected readonly IUnitOfWork UnitOfWork; - protected readonly IMapper Mapper; - private readonly DbConnection _connection; - private bool _disposed; + protected readonly DbConnection _connection; + protected readonly DataContext _context; + protected readonly IUnitOfWork _unitOfWork; + + + protected const string CacheDirectory = "C:/kavita/config/cache/"; + protected const string CoverImageDirectory = "C:/kavita/config/covers/"; + protected const string BackupDirectory = "C:/kavita/config/backups/"; + protected const string LogDirectory = "C:/kavita/config/logs/"; + protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; + protected const string SiteThemeDirectory = "C:/kavita/config/themes/"; + protected const string TempDirectory = "C:/kavita/config/temp/"; + protected const string DataDirectory = "C:/data/"; protected AbstractDbTest() { - var contextOptions = new DbContextOptionsBuilder() + var contextOptions = new DbContextOptionsBuilder() .UseSqlite(CreateInMemoryDatabase()) - .EnableSensitiveDataLogging() .Options; - _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; - Context = new DataContext(contextOptions); - - Context.Database.EnsureCreated(); // Ensure DB schema is created - + _context = new DataContext(contextOptions); Task.Run(SeedDb).GetAwaiter().GetResult(); var config = new MapperConfiguration(cfg => cfg.AddProfile()); - Mapper = config.CreateMapper(); + var mapper = config.CreateMapper(); - GlobalConfiguration.Configuration.UseInMemoryStorage(); - UnitOfWork = new UnitOfWork(Context, Mapper, null); + _unitOfWork = new UnitOfWork(_context, mapper, null); } private static DbConnection CreateInMemoryDatabase() @@ -58,79 +60,47 @@ public abstract class AbstractDbTest : AbstractFsTest , IDisposable private async Task SeedDb() { - try - { - await Context.Database.EnsureCreatedAsync(); - var filesystem = CreateFileSystem(); + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); - await Seed.SeedSettings(Context, new DirectoryService(Substitute.For>(), filesystem)); + await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); - var setting = await Context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); - setting.Value = CacheDirectory; + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; - setting = await Context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); - setting.Value = BackupDirectory; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; - setting = await Context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); - setting.Value = BookmarkDirectory; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); + setting.Value = BookmarkDirectory; - setting = await Context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync(); - setting.Value = "10"; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync(); + setting.Value = "10"; - Context.ServerSetting.Update(setting); + _context.ServerSetting.Update(setting); - - Context.Library.Add(new LibraryBuilder("Manga") - .WithAllowMetadataMatching(true) - .WithFolderPath(new FolderPathBuilder(DataDirectory).Build()) - .Build()); - - await Context.SaveChangesAsync(); - - await Seed.SeedMetadataSettings(Context); - - return true; - } - catch (Exception ex) - { - Console.WriteLine($"[SeedDb] Error: {ex.Message}"); - return false; - } + _context.Library.Add(new LibraryBuilder("Manga") + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) + .Build()); + return await _context.SaveChangesAsync() > 0; } protected abstract Task ResetDb(); - public void Dispose() + protected static MockFileSystem CreateFileSystem() { - Dispose(true); - GC.SuppressFinalize(this); - } + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(BookmarkDirectory); + fileSystem.AddDirectory(SiteThemeDirectory); + fileSystem.AddDirectory(LogDirectory); + fileSystem.AddDirectory(TempDirectory); + fileSystem.AddDirectory(DataDirectory); - protected virtual void Dispose(bool disposing) - { - if (_disposed) return; - - if (disposing) - { - Context?.Dispose(); - _connection?.Dispose(); - } - - _disposed = true; - } - - /// - /// Add a role to an existing User. Commits. - /// - /// - /// - protected async Task AddUserWithRole(int userId, string roleName) - { - var role = new AppRole { Id = userId, Name = roleName, NormalizedName = roleName.ToUpper() }; - - await Context.Roles.AddAsync(role); - await Context.UserRoles.AddAsync(new AppUserRole { UserId = userId, RoleId = userId }); - - await Context.SaveChangesAsync(); + return fileSystem; } } diff --git a/API.Tests/AbstractFsTest.cs b/API.Tests/AbstractFsTest.cs deleted file mode 100644 index 965a7ad78..000000000 --- a/API.Tests/AbstractFsTest.cs +++ /dev/null @@ -1,44 +0,0 @@ - - -using System.IO; -using System.IO.Abstractions; -using System.IO.Abstractions.TestingHelpers; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Tests; - -public abstract class AbstractFsTest -{ - - protected static readonly string Root = Parser.NormalizePath(Path.GetPathRoot(Directory.GetCurrentDirectory())); - protected static readonly string ConfigDirectory = Root + "kavita/config/"; - protected static readonly string CacheDirectory = ConfigDirectory + "cache/"; - protected static readonly string CacheLongDirectory = ConfigDirectory + "cache-long/"; - protected static readonly string CoverImageDirectory = ConfigDirectory + "covers/"; - protected static readonly string BackupDirectory = ConfigDirectory + "backups/"; - protected static readonly string LogDirectory = ConfigDirectory + "logs/"; - protected static readonly string BookmarkDirectory = ConfigDirectory + "bookmarks/"; - protected static readonly string SiteThemeDirectory = ConfigDirectory + "themes/"; - protected static readonly string TempDirectory = ConfigDirectory + "temp/"; - protected static readonly string ThemesDirectory = ConfigDirectory + "theme"; - protected static readonly string DataDirectory = Root + "data/"; - - protected static MockFileSystem CreateFileSystem() - { - var fileSystem = new MockFileSystem(); - fileSystem.Directory.SetCurrentDirectory(Root + "kavita/"); - fileSystem.AddDirectory(Root + "kavita/config/"); - fileSystem.AddDirectory(CacheDirectory); - fileSystem.AddDirectory(CacheLongDirectory); - fileSystem.AddDirectory(CoverImageDirectory); - fileSystem.AddDirectory(BackupDirectory); - fileSystem.AddDirectory(BookmarkDirectory); - fileSystem.AddDirectory(SiteThemeDirectory); - fileSystem.AddDirectory(LogDirectory); - fileSystem.AddDirectory(TempDirectory); - fileSystem.AddDirectory(DataDirectory); - fileSystem.AddDirectory(ThemesDirectory); - - return fileSystem; - } -} diff --git a/API.Tests/Comparers/ChapterSortComparerTest.cs b/API.Tests/Comparers/ChapterSortComparerTest.cs index 39a68b3b0..220be052d 100644 --- a/API.Tests/Comparers/ChapterSortComparerTest.cs +++ b/API.Tests/Comparers/ChapterSortComparerTest.cs @@ -4,16 +4,15 @@ using Xunit; namespace API.Tests.Comparers; -public class ChapterSortComparerDefaultLastTest +public class ChapterSortComparerTest { [Theory] - [InlineData(new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] + [InlineData(new[] {1, 2, 0}, new[] {1, 2, 0})] [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] + [InlineData(new[] {1, 0, 0}, new[] {1, 0, 0})] public void ChapterSortTest(int[] input, int[] expected) { - Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray()); + Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray()); } } diff --git a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs b/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs index fbae46b59..df3934884 100644 --- a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs +++ b/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs @@ -4,7 +4,7 @@ using Xunit; namespace API.Tests.Comparers; -public class ChapterSortComparerDefaultFirstTests +public class ChapterSortComparerZeroFirstTests { [Theory] [InlineData(new[] {1, 2, 0}, new[] {0, 1, 2,})] @@ -12,13 +12,13 @@ public class ChapterSortComparerDefaultFirstTests [InlineData(new[] {1, 0, 0}, new[] {0, 0, 1})] public void ChapterSortComparerZeroFirstTest(int[] input, int[] expected) { - Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultFirst()).ToArray()); + Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray()); } [Theory] - [InlineData(new [] {1.0f, 0.5f, 0.3f}, new [] {0.3f, 0.5f, 1.0f})] - public void ChapterSortComparerZeroFirstTest_Doubles(float[] input, float[] expected) + [InlineData(new[] {1.0, 0.5, 0.3}, new[] {0.3, 0.5, 1.0})] + public void ChapterSortComparerZeroFirstTest_Doubles(double[] input, double[] expected) { - Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultFirst()).ToArray()); + Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray()); } } diff --git a/API.Tests/Comparers/SortComparerZeroLastTests.cs b/API.Tests/Comparers/SortComparerZeroLastTests.cs index 9a0722984..669ca6c37 100644 --- a/API.Tests/Comparers/SortComparerZeroLastTests.cs +++ b/API.Tests/Comparers/SortComparerZeroLastTests.cs @@ -7,11 +7,11 @@ namespace API.Tests.Comparers; public class SortComparerZeroLastTests { [Theory] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] + [InlineData(new[] {0, 1, 2,}, new[] {1, 2, 0})] [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] + [InlineData(new[] {0, 0, 1}, new[] {1, 0, 0})] public void SortComparerZeroLastTest(int[] input, int[] expected) { - Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray()); + Assert.Equal(expected, input.OrderBy(f => f, SortComparerZeroLast.Default).ToArray()); } } diff --git a/API.Tests/Converters/CronConverterTests.cs b/API.Tests/Converters/CronConverterTests.cs index 5568c89d0..4d26edef7 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/API.Tests/Converters/CronConverterTests.cs @@ -2,18 +2,16 @@ using Xunit; namespace API.Tests.Converters; -#nullable enable + public class CronConverterTests { [Theory] [InlineData("daily", "0 0 * * *")] [InlineData("disabled", "0 0 31 2 *")] [InlineData("weekly", "0 0 * * 1")] - [InlineData("0 0 31 2 *", "0 0 31 2 *")] - [InlineData("sdfgdf", "sdfgdf")] - [InlineData("* * * * *", "* * * * *")] - [InlineData(null, "0 0 * * *")] // daily - public void ConvertTest(string? input, string expected) + [InlineData("", "0 0 31 2 *")] + [InlineData("sdfgdf", "")] + public void ConvertTest(string input, string expected) { Assert.Equal(expected, CronConverter.ConvertToCronNotation(input)); } diff --git a/API.Tests/Data/AesopsFables.epub b/API.Tests/Data/AesopsFables.epub deleted file mode 100644 index d2ab9a8b2..000000000 Binary files a/API.Tests/Data/AesopsFables.epub and /dev/null differ diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/API.Tests/Extensions/ChapterListExtensionsTests.cs index f19a0cede..3b59f1b02 100644 --- a/API.Tests/Extensions/ChapterListExtensionsTests.cs +++ b/API.Tests/Extensions/ChapterListExtensionsTests.cs @@ -30,7 +30,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + Chapters = "0", Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -38,12 +38,12 @@ public class ChapterListExtensionsTests IsSpecial = false, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = "0" }; var chapterList = new List() { - CreateChapter("darker than black - Some special", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black - Some special", "0", CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -57,7 +57,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = "0", Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -65,12 +65,12 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = "0" }; var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -83,7 +83,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = "0", Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/detective comics #001.cbz", @@ -91,39 +91,13 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "detective comics", Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = "0" }; var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) - }; - - var actualChapter = chapterList.GetChapterByRange(info); - - Assert.Equal(chapterList[0], actualChapter); - } - - [Fact] - public void GetChapterByRange_On_FilenameChange_ShouldGetChapter() - { - var info = new ParserInfo() - { - Chapters = "1", - Edition = "", - Format = MangaFormat.Archive, - FullFilePath = "/manga/detective comics #001.cbz", - Filename = "detective comics #001.cbz", - IsSpecial = false, - Series = "detective comics", - Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume - }; - - var chapterList = new List() - { - CreateChapter("1", "1", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), false), + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -138,11 +112,11 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), + CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; - Assert.Equal(chapterList[0], chapterList.GetFirstChapterWithFiles()); + Assert.Equal(chapterList.First(), chapterList.GetFirstChapterWithFiles()); } [Fact] @@ -150,13 +124,13 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), + CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; - chapterList[0].Files = new List(); + chapterList.First().Files = new List(); - Assert.Equal(chapterList[^1], chapterList.GetFirstChapterWithFiles()); + Assert.Equal(chapterList.Last(), chapterList.GetFirstChapterWithFiles()); } @@ -177,11 +151,11 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; - chapterList[0].ReleaseDate = new DateTime(10, 1, 1, 0, 0, 0, DateTimeKind.Utc); + chapterList[0].ReleaseDate = new DateTime(10, 1, 1); chapterList[1].ReleaseDate = DateTime.MinValue; Assert.Equal(0, chapterList.MinimumReleaseYear()); @@ -192,12 +166,12 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; - chapterList[0].ReleaseDate = new DateTime(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); - chapterList[1].ReleaseDate = new DateTime(2012, 2, 1, 0, 0, 0, DateTimeKind.Utc); + chapterList[0].ReleaseDate = new DateTime(2002, 1, 1); + chapterList[1].ReleaseDate = new DateTime(2012, 2, 1); Assert.Equal(2002, chapterList.MinimumReleaseYear()); } diff --git a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs b/API.Tests/Extensions/EncodeFormatExtensionsTests.cs deleted file mode 100644 index a02de84aa..000000000 --- a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using Xunit; - -namespace API.Tests.Extensions; - -public class EncodeFormatExtensionsTests -{ - [Fact] - public void GetExtension_ShouldReturnCorrectExtensionForAllValues() - { - // Arrange - var expectedExtensions = new Dictionary - { - { EncodeFormat.PNG, ".png" }, - { EncodeFormat.WEBP, ".webp" }, - { EncodeFormat.AVIF, ".avif" } - }; - - // Act & Assert - foreach (var format in Enum.GetValues(typeof(EncodeFormat)).Cast()) - { - var extension = format.GetExtension(); - Assert.Equal(expectedExtensions[format], extension); - } - } - -} diff --git a/API.Tests/Extensions/EnumerableExtensionsTests.cs b/API.Tests/Extensions/EnumerableExtensionsTests.cs index bdd3433ae..e115d45f3 100644 --- a/API.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/API.Tests/Extensions/EnumerableExtensionsTests.cs @@ -74,10 +74,10 @@ public class EnumerableExtensionsTests new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"}, new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"} )] - [InlineData( - new[] {"01/001.jpg", "001.jpg"}, - new[] {"001.jpg", "01/001.jpg"} - )] + [InlineData( + new[] {"01/001.jpg", "001.jpg"}, + new[] {"001.jpg", "01/001.jpg"} + )] public void TestNaturalSort(string[] input, string[] expected) { Assert.Equal(expected, input.OrderByNatural(x => x).ToArray()); diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs index 227dd2b32..6ea35e471 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using API.Entities.Enums; @@ -7,6 +6,7 @@ using API.Extensions; using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner.Parser; +using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -18,8 +18,9 @@ public class ParserInfoListExtensions private readonly IDefaultParser _defaultParser; public ParserInfoListExtensions() { - var ds = new DirectoryService(Substitute.For>(), new MockFileSystem()); - _defaultParser = new BasicParser(ds, new ImageParser(ds)); + _defaultParser = + new DefaultParser(new DirectoryService(Substitute.For>(), + new MockFileSystem())); } [Theory] @@ -32,7 +33,7 @@ public class ParserInfoListExtensions [Theory] [InlineData(new[] {@"Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)] - [InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)] + [InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)] [InlineData(new[] {@"Cynthia The Mission v20 c12-20 [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)] public void HasInfoTest(string[] inputInfos, string[] inputChapters, bool expectedHasInfo) { @@ -40,8 +41,8 @@ public class ParserInfoListExtensions foreach (var filename in inputInfos) { infos.Add(_defaultParser.Parse( - Path.Join("E:/Manga/Cynthia the Mission/", filename), - "E:/Manga/", "E:/Manga/", LibraryType.Manga)); + filename, + string.Empty)); } var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList(); @@ -51,26 +52,4 @@ public class ParserInfoListExtensions Assert.Equal(expectedHasInfo, infos.HasInfo(chapter)); } - - [Fact] - public void HasInfoTest_SuccessWhenSpecial() - { - var infos = new[] - { - _defaultParser.Parse( - "E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip", - "E:/Manga/", "E:/Manga/", LibraryType.Manga) - }; - - var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"} - .Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()) - .ToList(); - var chapter = new ChapterBuilder("Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip") - .WithRange("Cynthia The Mission The Special SP01 [Desudesu&Brolen]") - .WithFiles(files) - .WithIsSpecial(true) - .Build(); - - Assert.True(infos.HasInfo(chapter)); - } } diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 96d74b46d..230028d44 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; +using API.Data; using API.Data.Misc; using API.Entities; using API.Entities.Enums; -using API.Entities.Person; +using API.Entities.Metadata; +using API.Extensions; using API.Extensions.QueryExtensions; using API.Helpers.Builders; using Xunit; @@ -43,17 +45,17 @@ public class QueryableExtensionsTests [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_CollectionTag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { - var items = new List() + var items = new List() { - new AppUserCollectionBuilder("Test") - .WithItem(new SeriesBuilder("S1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) + new CollectionTagBuilder("Test") + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) .Build(), - new AppUserCollectionBuilder("Test 2") - .WithItem(new SeriesBuilder("S2").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()).Build()) - .WithItem(new SeriesBuilder("S1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()).Build()) + new CollectionTagBuilder("Test 2") + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()) + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) .Build(), - new AppUserCollectionBuilder("Test 3") - .WithItem(new SeriesBuilder("S3").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()).Build()) + new CollectionTagBuilder("Test 3") + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()) .Build(), }; @@ -67,7 +69,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 2)] + [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() @@ -94,7 +96,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 2)] + [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() @@ -121,46 +123,29 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 2)] - public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedPeopleCount) + [InlineData(false, 1)] + public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { - // Arrange - var items = new List + var items = new List() { - CreatePersonWithSeriesMetadata("Test1", AgeRating.Teen), - CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, AgeRating.Teen), // 2 series on this person, restrict will still allow access - CreatePersonWithSeriesMetadata("Test3", AgeRating.X18Plus) + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) + .Build(), + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()) + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build()) + .Build(), + new PersonBuilder("Test", PersonRole.Character) + .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build()) + .Build(), }; - var ageRestriction = new AgeRestriction + var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() { AgeRating = AgeRating.Teen, IncludeUnknowns = includeUnknowns - }; - - // Act - var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(ageRestriction); - - // Assert - Assert.Equal(expectedPeopleCount, filtered.Count()); - } - - private static Person CreatePersonWithSeriesMetadata(string name, params AgeRating[] ageRatings) - { - var person = new PersonBuilder(name).Build(); - - foreach (var ageRating in ageRatings) - { - var seriesMetadata = new SeriesMetadataBuilder().WithAgeRating(ageRating).Build(); - person.SeriesMetadataPeople.Add(new SeriesMetadataPeople - { - SeriesMetadata = seriesMetadata, - Person = person, - Role = PersonRole.Character // Role is now part of the relationship - }); - } - - return person; + }); + Assert.Equal(expectedCount, filtered.Count()); } [Theory] diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index adaecfba5..6a706e892 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using API.Comparators; +using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; using Xunit; namespace API.Tests.Extensions; @@ -15,23 +17,22 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithCoverImage("Special 1") .WithIsSpecial(true) - .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithCoverImage("Special 2") .WithIsSpecial(true) - .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .Build(); foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Special 1", series.GetCoverImage()); @@ -42,8 +43,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("13") .WithCoverImage("Chapter 13") .Build()) @@ -58,7 +59,7 @@ public class SeriesExtensionsTests .WithVolume(new VolumeBuilder("2") .WithName("Volume 2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithCoverImage("Volume 2") .Build()) .Build()) @@ -66,83 +67,12 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); } - [Fact] - public void GetCoverImage_LooseChapters_WithSub1_Chapter() - { - var series = new SeriesBuilder("Test 1") - .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("-1") - .WithCoverImage("Chapter -1") - .Build()) - .WithChapter(new ChapterBuilder("0.5") - .WithCoverImage("Chapter 0.5") - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithCoverImage("Chapter 2") - .Build()) - .WithChapter(new ChapterBuilder("1") - .WithCoverImage("Chapter 1") - .Build()) - .WithChapter(new ChapterBuilder("3") - .WithCoverImage("Chapter 3") - .Build()) - .WithChapter(new ChapterBuilder("4AU") - .WithCoverImage("Chapter 4AU") - .Build()) - .Build()) - - .Build(); - - - Assert.Equal("Chapter 1", series.GetCoverImage()); - } - - /// - /// Checks the case where there are specials and loose leafs, loose leaf chapters should be preferred - /// - [Fact] - public void GetCoverImage_LooseChapters_WithSub1_Chapter_WithSpecials() - { - var series = new SeriesBuilder("Test 1") - .WithFormat(MangaFormat.Archive) - - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithName(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("I am a Special") - .WithCoverImage("I am a Special") - .Build()) - .WithChapter(new ChapterBuilder("I am a Special 2") - .WithCoverImage("I am a Special 2") - .Build()) - .Build()) - - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("0.5") - .WithCoverImage("Chapter 0.5") - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithCoverImage("Chapter 2") - .Build()) - .WithChapter(new ChapterBuilder("1") - .WithCoverImage("Chapter 1") - .Build()) - .Build()) - - .Build(); - - - Assert.Equal("Chapter 1", series.GetCoverImage()); - } - [Fact] public void GetCoverImage_JustVolumes() { @@ -151,14 +81,14 @@ public class SeriesExtensionsTests .WithVolume(new VolumeBuilder("1") .WithName("Volume 1") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithCoverImage("Volume 1 Chapter 1") .Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithName("Volume 2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithCoverImage("Volume 2") .Build()) .Build()) @@ -179,48 +109,19 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); } - [Fact] - public void GetCoverImage_JustVolumes_ButVolume0() - { - var series = new SeriesBuilder("Test 1") - .WithFormat(MangaFormat.Archive) - - .WithVolume(new VolumeBuilder("0") - .WithName("Volume 0") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) - .WithCoverImage("Volume 0") - .Build()) - .Build()) - - .WithVolume(new VolumeBuilder("1") - .WithName("Volume 1") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) - .WithCoverImage("Volume 1") - .Build()) - .Build()) - .Build(); - - foreach (var vol in series.Volumes) - { - vol.CoverImage = vol.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default)?.CoverImage; - } - - Assert.Equal("Volume 1", series.GetCoverImage()); - } - [Fact] public void GetCoverImage_JustSpecials_WithDecimal() { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("2.5") .WithIsSpecial(false) .WithCoverImage("Special 1") @@ -234,7 +135,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Special 2", series.GetCoverImage()); @@ -245,8 +146,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("2.5") .WithIsSpecial(false) .WithCoverImage("Chapter 2.5") @@ -255,19 +156,16 @@ public class SeriesExtensionsTests .WithIsSpecial(false) .WithCoverImage("Chapter 2") .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(true) .WithCoverImage("Special 1") - .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) - .Build()) + .Build()) .Build(); foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Chapter 2", series.GetCoverImage()); @@ -278,8 +176,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("2.5") .WithIsSpecial(false) .WithCoverImage("Chapter 2.5") @@ -288,17 +186,14 @@ public class SeriesExtensionsTests .WithIsSpecial(false) .WithCoverImage("Chapter 2") .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(true) .WithCoverImage("Special 3") - .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") .Build()) @@ -307,7 +202,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -318,8 +213,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("2.5") .WithIsSpecial(false) .WithCoverImage("Chapter 2.5") @@ -328,17 +223,14 @@ public class SeriesExtensionsTests .WithIsSpecial(false) .WithCoverImage("Chapter 2") .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(true) .WithCoverImage("Special 1") - .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") .Build()) @@ -347,7 +239,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -358,8 +250,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Ippo") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1426") .WithIsSpecial(false) .WithCoverImage("Chapter 1426") @@ -368,24 +260,21 @@ public class SeriesExtensionsTests .WithIsSpecial(false) .WithCoverImage("Chapter 1425") .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(true) - .WithCoverImage("Special 3") - .WithSortOrder(Parser.SpecialVolumeNumber + 1) + .WithCoverImage("Special 1") .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") .Build()) .Build()) .WithVolume(new VolumeBuilder("137") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 137") .Build()) @@ -394,7 +283,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -405,8 +294,8 @@ public class SeriesExtensionsTests { var series = new SeriesBuilder("Test 1") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("2.5") .WithIsSpecial(false) .WithCoverImage("Chapter 2.5") @@ -417,8 +306,8 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("4") - .WithMinNumber(4) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithNumber(4) + .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 4") .Build()) @@ -427,77 +316,11 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Chapter 2", series.GetCoverImage()); } - /// - /// Ensure that Series cover is issue 1, when there are less than 1 entities and specials - /// - [Fact] - public void GetCoverImage_LessThanIssue1() - { - var series = new SeriesBuilder("Test 1") - .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("0") - .WithIsSpecial(false) - .WithCoverImage("Chapter 0") - .Build()) - .WithChapter(new ChapterBuilder("1") - .WithIsSpecial(false) - .WithCoverImage("Chapter 1") - .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithMinNumber(4) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) - .WithIsSpecial(false) - .WithCoverImage("Volume 4") - .Build()) - .Build()) - .Build(); - - Assert.Equal("Chapter 1", series.GetCoverImage()); - } - - /// - /// Ensure that Series cover is issue 1, when there are less than 1 entities and specials - /// - [Fact] - public void GetCoverImage_LessThanIssue1_WithNegative() - { - var series = new SeriesBuilder("Test 1") - .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithName(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("-1") - .WithIsSpecial(false) - .WithCoverImage("Chapter -1") - .Build()) - .WithChapter(new ChapterBuilder("0") - .WithIsSpecial(false) - .WithCoverImage("Chapter 0") - .Build()) - .WithChapter(new ChapterBuilder("1") - .WithIsSpecial(false) - .WithCoverImage("Chapter 1") - .Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithMinNumber(4) - .WithChapter(new ChapterBuilder(Parser.DefaultChapter) - .WithIsSpecial(false) - .WithCoverImage("Volume 4") - .Build()) - .Build()) - .Build(); - - Assert.Equal("Chapter 1", series.GetCoverImage()); - } - } diff --git a/API.Tests/Extensions/SeriesFilterTests.cs b/API.Tests/Extensions/SeriesFilterTests.cs index ba42be8a1..2774ad78e 100644 --- a/API.Tests/Extensions/SeriesFilterTests.cs +++ b/API.Tests/Extensions/SeriesFilterTests.cs @@ -1,1338 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Threading.Tasks; -using API.DTOs; using API.DTOs.Filtering.v2; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; using API.Extensions.QueryExtensions.Filtering; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.SignalR; -using Kavita.Common; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using NSubstitute; using Xunit; namespace API.Tests.Extensions; public class SeriesFilterTests : AbstractDbTest { - protected override async Task ResetDb() + + protected override Task ResetDb() { - Context.Series.RemoveRange(Context.Series); - Context.AppUser.RemoveRange(Context.AppUser); - await Context.SaveChangesAsync(); + return Task.CompletedTask; } - #region HasProgress - - private async Task SetupHasProgress() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("None").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Partial").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Full").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - - // Create read progress on Partial and Full - var readerService = new ReaderService(UnitOfWork, Substitute.For>(), - Substitute.For(), Substitute.For(), - Substitute.For(), Substitute.For()); - - // Select Partial and set pages read to 5 on first chapter - var partialSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2); - var partialChapter = partialSeries.Volumes.First().Chapters.First(); - - Assert.True(await readerService.SaveReadingProgress(new ProgressDto() - { - ChapterId = partialChapter.Id, - LibraryId = 1, - SeriesId = partialSeries.Id, - PageNum = 5, - VolumeId = partialChapter.VolumeId - }, user.Id)); - - // Select Full and set pages read to 10 on first chapter - var fullSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(3); - var fullChapter = fullSeries.Volumes.First().Chapters.First(); - - Assert.True(await readerService.SaveReadingProgress(new ProgressDto() - { - ChapterId = fullChapter.Id, - LibraryId = 1, - SeriesId = fullSeries.Id, - PageNum = 10, - VolumeId = fullChapter.VolumeId - }, user.Id)); - - return user; - } - - [Fact] - public async Task HasProgress_LessThan50_ShouldReturnSingle() - { - var user = await SetupHasProgress(); - - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 50, user.Id) - .ToListAsync(); - - Assert.Single(queryResult); - Assert.Equal("None", queryResult.First().Name); - } - - [Fact] - public async Task HasProgress_LessThanOrEqual50_ShouldReturnTwo() - { - var user = await SetupHasProgress(); - - // Query series with progress <= 50% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 50, user.Id) - .ToListAsync(); - - Assert.Equal(2, queryResult.Count); - Assert.Contains(queryResult, s => s.Name == "None"); - Assert.Contains(queryResult, s => s.Name == "Partial"); - } - - [Fact] - public async Task HasProgress_GreaterThan50_ShouldReturnFull() - { - var user = await SetupHasProgress(); - - // Query series with progress > 50% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.GreaterThan, 50, user.Id) - .ToListAsync(); - - Assert.Single(queryResult); - Assert.Equal("Full", queryResult.First().Name); - } - - [Fact] - public async Task HasProgress_Equal100_ShouldReturnFull() - { - var user = await SetupHasProgress(); - - // Query series with progress == 100% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.Equal, 100, user.Id) - .ToListAsync(); - - Assert.Single(queryResult); - Assert.Equal("Full", queryResult.First().Name); - } - - [Fact] - public async Task HasProgress_LessThan100_ShouldReturnTwo() - { - var user = await SetupHasProgress(); - - // Query series with progress < 100% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) - .ToListAsync(); - - Assert.Equal(2, queryResult.Count); - Assert.Contains(queryResult, s => s.Name == "None"); - Assert.Contains(queryResult, s => s.Name == "Partial"); - } - - [Fact] - public async Task HasProgress_LessThanOrEqual100_ShouldReturnAll() - { - var user = await SetupHasProgress(); - - // Query series with progress <= 100% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 100, user.Id) - .ToListAsync(); - - Assert.Equal(3, queryResult.Count); - Assert.Contains(queryResult, s => s.Name == "None"); - Assert.Contains(queryResult, s => s.Name == "Partial"); - Assert.Contains(queryResult, s => s.Name == "Full"); - } - - [Fact] - public async Task HasProgress_LessThan100_WithProgress99_99_ShouldReturnSeries() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("AlmostFull").WithPages(100) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(100).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - var readerService = new ReaderService(UnitOfWork, Substitute.For>(), - Substitute.For(), Substitute.For(), - Substitute.For(), Substitute.For()); - - // Set progress to 99.99% (99/100 pages read) - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - var chapter = series.Volumes.First().Chapters.First(); - - Assert.True(await readerService.SaveReadingProgress(new ProgressDto() - { - ChapterId = chapter.Id, - LibraryId = 1, - SeriesId = series.Id, - PageNum = 99, - VolumeId = chapter.VolumeId - }, user.Id)); - - // Query series with progress < 100% - var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) - .ToListAsync(); - - Assert.Single(queryResult); - Assert.Equal("AlmostFull", queryResult.First().Name); - } - #endregion - #region HasLanguage - private async Task SetupHasLanguage() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("English").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithLanguage("en").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("French").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithLanguage("fr").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Spanish").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithLanguage("es").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasLanguage_Equal_Works() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Equal, ["en"]).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("en", foundSeries[0].Metadata.Language); - } - - [Fact] - public async Task HasLanguage_NotEqual_Works() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.NotEqual, ["en"]).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.DoesNotContain(foundSeries, s => s.Metadata.Language == "en"); - } - - [Fact] - public async Task HasLanguage_Contains_Works() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Contains, ["en", "fr"]).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Metadata.Language == "en"); - Assert.Contains(foundSeries, s => s.Metadata.Language == "fr"); - } - - [Fact] - public async Task HasLanguage_NotContains_Works() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.NotContains, ["en", "fr"]).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("es", foundSeries[0].Metadata.Language); - } - - [Fact] - public async Task HasLanguage_MustContains_Works() - { - await SetupHasLanguage(); - - // Since "MustContains" matches all the provided languages, no series should match in this case. - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.MustContains, ["en", "fr"]).ToListAsync(); - Assert.Empty(foundSeries); - - // Single language should work. - foundSeries = await Context.Series.HasLanguage(true, FilterComparison.MustContains, ["en"]).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("en", foundSeries[0].Metadata.Language); - } - - [Fact] - public async Task HasLanguage_Matches_Works() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Matches, ["e"]).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains("en", foundSeries.Select(s => s.Metadata.Language)); - Assert.Contains("es", foundSeries.Select(s => s.Metadata.Language)); - } - - [Fact] - public async Task HasLanguage_DisabledCondition_ReturnsAll() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(false, FilterComparison.Equal, ["en"]).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasLanguage_EmptyLanguageList_ReturnsAll() - { - await SetupHasLanguage(); - - var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Equal, new List()).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasLanguage_UnsupportedComparison_ThrowsException() - { - await SetupHasLanguage(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasLanguage(true, FilterComparison.GreaterThan, ["en"]).ToListAsync(); - }); - } - - #endregion - - #region HasAverageRating - - private async Task SetupHasAverageRating() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("None").WithPages(10) - .WithExternalMetadata(new ExternalSeriesMetadataBuilder().WithAverageExternalRating(-1).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Partial").WithPages(10) - .WithExternalMetadata(new ExternalSeriesMetadataBuilder().WithAverageExternalRating(50).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Full").WithPages(10) - .WithExternalMetadata(new ExternalSeriesMetadataBuilder().WithAverageExternalRating(100).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasAverageRating_Equal_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.Equal, 100).ToListAsync(); - Assert.Single(series); - Assert.Equal("Full", series[0].Name); - } - - [Fact] - public async Task HasAverageRating_GreaterThan_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.GreaterThan, 50).ToListAsync(); - Assert.Single(series); - Assert.Equal("Full", series[0].Name); - } - - [Fact] - public async Task HasAverageRating_GreaterThanEqual_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.GreaterThanEqual, 50).ToListAsync(); - Assert.Equal(2, series.Count); - Assert.Contains(series, s => s.Name == "Partial"); - Assert.Contains(series, s => s.Name == "Full"); - } - - [Fact] - public async Task HasAverageRating_LessThan_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.LessThan, 50).ToListAsync(); - Assert.Single(series); - Assert.Equal("None", series[0].Name); - } - - [Fact] - public async Task HasAverageRating_LessThanEqual_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.LessThanEqual, 50).ToListAsync(); - Assert.Equal(2, series.Count); - Assert.Contains(series, s => s.Name == "None"); - Assert.Contains(series, s => s.Name == "Partial"); - } - - [Fact] - public async Task HasAverageRating_NotEqual_Works() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.NotEqual, 100).ToListAsync(); - Assert.Equal(2, series.Count); - Assert.DoesNotContain(series, s => s.Name == "Full"); - } - - [Fact] - public async Task HasAverageRating_ConditionFalse_ReturnsAll() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(false, FilterComparison.Equal, 100).ToListAsync(); - Assert.Equal(3, series.Count); - } - - [Fact] - public async Task HasAverageRating_NotSet_IsHandled() - { - await SetupHasAverageRating(); - - var series = await Context.Series.HasAverageRating(true, FilterComparison.Equal, -1).ToListAsync(); - Assert.Single(series); - Assert.Equal("None", series[0].Name); - } - - [Fact] - public async Task HasAverageRating_ThrowsForInvalidComparison() - { - await SetupHasAverageRating(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasAverageRating(true, FilterComparison.Contains, 50).ToListAsync(); - }); - } - - [Fact] - public async Task HasAverageRating_ThrowsForOutOfRangeComparison() - { - await SetupHasAverageRating(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasAverageRating(true, (FilterComparison)999, 50).ToListAsync(); - }); - } - - #endregion - - # region HasPublicationStatus - - private async Task SetupHasPublicationStatus() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("Cancelled").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Cancelled).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("OnGoing").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.OnGoing).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Completed").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Completed).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasPublicationStatus_Equal_Works() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("Cancelled", foundSeries[0].Name); - } - - [Fact] - public async Task HasPublicationStatus_Contains_Works() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Contains, new List { PublicationStatus.Cancelled, PublicationStatus.Completed }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "Cancelled"); - Assert.Contains(foundSeries, s => s.Name == "Completed"); - } - - [Fact] - public async Task HasPublicationStatus_NotContains_Works() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.NotContains, new List { PublicationStatus.Cancelled }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "OnGoing"); - Assert.Contains(foundSeries, s => s.Name == "Completed"); - } - - [Fact] - public async Task HasPublicationStatus_NotEqual_Works() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.NotEqual, new List { PublicationStatus.OnGoing }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "Cancelled"); - Assert.Contains(foundSeries, s => s.Name == "Completed"); - } - - [Fact] - public async Task HasPublicationStatus_ConditionFalse_ReturnsAll() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(false, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasPublicationStatus_EmptyPubStatuses_ReturnsAll() - { - await SetupHasPublicationStatus(); - - var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List()).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasPublicationStatus_ThrowsForInvalidComparison() - { - await SetupHasPublicationStatus(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasPublicationStatus(true, FilterComparison.BeginsWith, new List { PublicationStatus.Cancelled }).ToListAsync(); - }); - } - - [Fact] - public async Task HasPublicationStatus_ThrowsForOutOfRangeComparison() - { - await SetupHasPublicationStatus(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasPublicationStatus(true, (FilterComparison)999, new List { PublicationStatus.Cancelled }).ToListAsync(); - }); - } - #endregion - - #region HasAgeRating - private async Task SetupHasAgeRating() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("Unknown").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("G").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.G).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Mature").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Mature).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasAgeRating_Equal_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Equal, [AgeRating.G]).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("G", foundSeries[0].Name); - } - - [Fact] - public async Task HasAgeRating_Contains_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Contains, new List { AgeRating.G, AgeRating.Mature }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "G"); - Assert.Contains(foundSeries, s => s.Name == "Mature"); - } - - [Fact] - public async Task HasAgeRating_NotContains_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.NotContains, new List { AgeRating.Unknown }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "G"); - Assert.Contains(foundSeries, s => s.Name == "Mature"); - } - - [Fact] - public async Task HasAgeRating_NotEqual_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.NotEqual, new List { AgeRating.G }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "Unknown"); - Assert.Contains(foundSeries, s => s.Name == "Mature"); - } - - [Fact] - public async Task HasAgeRating_GreaterThan_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.GreaterThan, new List { AgeRating.Unknown }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "G"); - Assert.Contains(foundSeries, s => s.Name == "Mature"); - } - - [Fact] - public async Task HasAgeRating_GreaterThanEqual_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.GreaterThanEqual, new List { AgeRating.G }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "G"); - Assert.Contains(foundSeries, s => s.Name == "Mature"); - } - - [Fact] - public async Task HasAgeRating_LessThan_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.LessThan, new List { AgeRating.Mature }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "Unknown"); - Assert.Contains(foundSeries, s => s.Name == "G"); - } - - [Fact] - public async Task HasAgeRating_LessThanEqual_Works() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.LessThanEqual, new List { AgeRating.G }).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "Unknown"); - Assert.Contains(foundSeries, s => s.Name == "G"); - } - - [Fact] - public async Task HasAgeRating_ConditionFalse_ReturnsAll() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(false, FilterComparison.Equal, new List { AgeRating.G }).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasAgeRating_EmptyRatings_ReturnsAll() - { - await SetupHasAgeRating(); - - var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Equal, new List()).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - [Fact] - public async Task HasAgeRating_ThrowsForInvalidComparison() + public async Task HasLanguage_Works() { - await SetupHasAgeRating(); + var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Contains, new List() { }).ToListAsync(); - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasAgeRating(true, FilterComparison.BeginsWith, new List { AgeRating.G }).ToListAsync(); - }); } - [Fact] - public async Task HasAgeRating_ThrowsForOutOfRangeComparison() - { - await SetupHasAgeRating(); - - await Assert.ThrowsAsync(async () => - { - await Context.Series.HasAgeRating(true, (FilterComparison)999, new List { AgeRating.G }).ToListAsync(); - }); - } - - #endregion - - #region HasReleaseYear - - private async Task SetupHasReleaseYear() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("2000").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithReleaseYear(2000).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("2020").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithReleaseYear(2020).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("2025").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithReleaseYear(2025).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasReleaseYear_Equal_Works() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.Equal, 2020).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("2020", foundSeries[0].Name); - } - - [Fact] - public async Task HasReleaseYear_GreaterThan_Works() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.GreaterThan, 2000).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "2020"); - Assert.Contains(foundSeries, s => s.Name == "2025"); - } - - [Fact] - public async Task HasReleaseYear_LessThan_Works() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.LessThan, 2025).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - Assert.Contains(foundSeries, s => s.Name == "2000"); - Assert.Contains(foundSeries, s => s.Name == "2020"); - } - - [Fact] - public async Task HasReleaseYear_IsInLast_Works() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsInLast, 5).ToListAsync(); - Assert.Equal(2, foundSeries.Count); - } - - [Fact] - public async Task HasReleaseYear_IsNotInLast_Works() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsNotInLast, 5).ToListAsync(); - Assert.Single(foundSeries); - Assert.Contains(foundSeries, s => s.Name == "2000"); - } - - [Fact] - public async Task HasReleaseYear_ConditionFalse_ReturnsAll() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(false, FilterComparison.Equal, 2020).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasReleaseYear_ReleaseYearNull_ReturnsAll() - { - await SetupHasReleaseYear(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.Equal, null).ToListAsync(); - Assert.Equal(3, foundSeries.Count); - } - - [Fact] - public async Task HasReleaseYear_IsEmpty_Works() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("EmptyYear").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithReleaseYear(0).Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsEmpty, 0).ToListAsync(); - Assert.Single(foundSeries); - Assert.Equal("EmptyYear", foundSeries[0].Name); - } - - - #endregion - - #region HasRating - - private async Task SetupHasRating() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("No Rating").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("0 Rating").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("4.5 Rating").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - var ratingService = new RatingService(UnitOfWork, Substitute.For(), Substitute.For>()); - - // Select 0 Rating - var zeroRating = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2); - Assert.NotNull(zeroRating); - - Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto() - { - SeriesId = zeroRating.Id, - UserRating = 0 - })); - - // Select 4.5 Rating - var partialRating = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(3); - - Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto() - { - SeriesId = partialRating.Id, - UserRating = 4.5f - })); - - return user; - } - - [Fact] - public async Task HasRating_Equal_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.Equal, 4.5f, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("4.5 Rating", foundSeries[0].Name); - } - - [Fact] - public async Task HasRating_GreaterThan_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.GreaterThan, 0, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("4.5 Rating", foundSeries[0].Name); - } - - [Fact] - public async Task HasRating_LessThan_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.LessThan, 4.5f, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("0 Rating", foundSeries[0].Name); - } - - [Fact] - public async Task HasRating_IsEmpty_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.IsEmpty, 0, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("No Rating", foundSeries[0].Name); - } - - [Fact] - public async Task HasRating_GreaterThanEqual_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.GreaterThanEqual, 4.5f, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("4.5 Rating", foundSeries[0].Name); - } - - [Fact] - public async Task HasRating_LessThanEqual_Works() - { - var user = await SetupHasRating(); - - var foundSeries = await Context.Series - .HasRating(true, FilterComparison.LessThanEqual, 0, user.Id) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("0 Rating", foundSeries[0].Name); - } - - #endregion - - #region HasAverageReadTime - - - - #endregion - - #region HasReadLast - - - - #endregion - - #region HasReadingDate - - - - #endregion - - #region HasTags - - - - #endregion - - #region HasPeople - - - - #endregion - - #region HasGenre - - - - #endregion - - #region HasFormat - - - - #endregion - - #region HasCollectionTags - - - - #endregion - - #region HasName - - private async Task SetupHasName() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("Don't Toy With Me, Miss Nagatoro").WithLocalizedName("Ijiranaide, Nagatoro-san").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("My Dress-Up Darling").WithLocalizedName("Sono Bisque Doll wa Koi wo Suru").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasName_Equal_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.Equal, "My Dress-Up Darling") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("My Dress-Up Darling", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_Equal_LocalizedName_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.Equal, "Ijiranaide, Nagatoro-san") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Don't Toy With Me, Miss Nagatoro", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_BeginsWith_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.BeginsWith, "My Dress") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("My Dress-Up Darling", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_BeginsWith_LocalizedName_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.BeginsWith, "Sono Bisque") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("My Dress-Up Darling", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_EndsWith_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.EndsWith, "Nagatoro") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Don't Toy With Me, Miss Nagatoro", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_Matches_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.Matches, "Toy With Me") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Don't Toy With Me, Miss Nagatoro", foundSeries[0].Name); - } - - [Fact] - public async Task HasName_NotEqual_Works() - { - await SetupHasName(); - - var foundSeries = await Context.Series - .HasName(true, FilterComparison.NotEqual, "My Dress-Up Darling") - .ToListAsync(); - - Assert.Equal(2, foundSeries.Count); - Assert.Equal("Don't Toy With Me, Miss Nagatoro", foundSeries[0].Name); - } - - - #endregion - - #region HasSummary - - private async Task SetupHasSummary() - { - var library = new LibraryBuilder("Manga") - .WithSeries(new SeriesBuilder("Hippos").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithSummary("I like hippos").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Apples").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithSummary("I like apples").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("Ducks").WithPages(10) - .WithMetadata(new SeriesMetadataBuilder().WithSummary("I like ducks").Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("No Summary").WithPages(10) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(10).Build()) - .Build()) - .Build()) - .Build(); - var user = new AppUserBuilder("user", "user@gmail.com") - .WithLibrary(library) - .Build(); - - Context.Users.Add(user); - Context.Library.Add(library); - await Context.SaveChangesAsync(); - - return user; - } - - [Fact] - public async Task HasSummary_Equal_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.Equal, "I like hippos") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Hippos", foundSeries[0].Name); - } - - [Fact] - public async Task HasSummary_BeginsWith_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.BeginsWith, "I like h") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Hippos", foundSeries[0].Name); - } - - [Fact] - public async Task HasSummary_EndsWith_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.EndsWith, "apples") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Apples", foundSeries[0].Name); - } - - [Fact] - public async Task HasSummary_Matches_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.Matches, "like ducks") - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("Ducks", foundSeries[0].Name); - } - - [Fact] - public async Task HasSummary_NotEqual_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.NotEqual, "I like ducks") - .ToListAsync(); - - Assert.Equal(3, foundSeries.Count); - Assert.DoesNotContain(foundSeries, s => s.Name == "Ducks"); - } - - [Fact] - public async Task HasSummary_IsEmpty_Works() - { - await SetupHasSummary(); - - var foundSeries = await Context.Series - .HasSummary(true, FilterComparison.IsEmpty, string.Empty) - .ToListAsync(); - - Assert.Single(foundSeries); - Assert.Equal("No Summary", foundSeries[0].Name); - } - - #endregion - - - #region HasPath - - - - #endregion - - - #region HasFilePath - - - #endregion } diff --git a/API.Tests/Extensions/VersionExtensionTests.cs b/API.Tests/Extensions/VersionExtensionTests.cs deleted file mode 100644 index e19fd7312..000000000 --- a/API.Tests/Extensions/VersionExtensionTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using API.Extensions; -using Xunit; - -namespace API.Tests.Extensions; - -public class VersionHelperTests -{ - [Fact] - public void CompareWithoutRevision_ShouldReturnTrue_WhenMajorMinorBuildMatch() - { - // Arrange - var v1 = new Version(1, 2, 3, 4); - var v2 = new Version(1, 2, 3, 5); - - // Act - var result = v1.CompareWithoutRevision(v2); - - // Assert - Assert.True(result); - } - - [Fact] - public void CompareWithoutRevision_ShouldHandleBuildlessVersions() - { - // Arrange - var v1 = new Version(1, 2); - var v2 = new Version(1, 2); - - // Act - var result = v1.CompareWithoutRevision(v2); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData(1, 2, 3, 1, 2, 4)] - [InlineData(1, 2, 3, 1, 2, 0)] - public void CompareWithoutRevision_ShouldReturnFalse_WhenBuildDiffers( - int major1, int minor1, int build1, - int major2, int minor2, int build2) - { - var v1 = new Version(major1, minor1, build1); - var v2 = new Version(major2, minor2, build2); - - var result = v1.CompareWithoutRevision(v2); - - Assert.False(result); - } - - [Theory] - [InlineData(1, 2, 3, 1, 3, 3)] - [InlineData(1, 2, 3, 1, 0, 3)] - public void CompareWithoutRevision_ShouldReturnFalse_WhenMinorDiffers( - int major1, int minor1, int build1, - int major2, int minor2, int build2) - { - var v1 = new Version(major1, minor1, build1); - var v2 = new Version(major2, minor2, build2); - - var result = v1.CompareWithoutRevision(v2); - - Assert.False(result); - } - - [Theory] - [InlineData(1, 2, 3, 2, 2, 3)] - [InlineData(1, 2, 3, 0, 2, 3)] - public void CompareWithoutRevision_ShouldReturnFalse_WhenMajorDiffers( - int major1, int minor1, int build1, - int major2, int minor2, int build2) - { - var v1 = new Version(major1, minor1, build1); - var v2 = new Version(major2, minor2, build2); - - var result = v1.CompareWithoutRevision(v2); - - Assert.False(result); - } -} diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs index bbb8f215c..2db82eeda 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs @@ -3,6 +3,7 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; +using API.Tests.Helpers; using Xunit; namespace API.Tests.Extensions; @@ -20,44 +21,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1").Build()) - .Build(), - - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(), }; - var v = volumes.GetCoverImage(MangaFormat.Archive); - Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber); - } - - [Fact] - public void GetCoverImage_ChoosesVolume1_WhenHalf() - { - var volumes = new List() - { - new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) - .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("0.5").Build()) - .Build(), - - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) - .Build(), - }; - - var v = volumes.GetCoverImage(MangaFormat.Archive); - Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber); + Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number); } [Fact] @@ -69,14 +39,9 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1").Build()) - .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(), }; @@ -92,14 +57,9 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1").Build()) - .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(), }; @@ -115,14 +75,9 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1").Build()) - .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(), }; @@ -140,12 +95,7 @@ public class VolumeListExtensionsTests .Build(), new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").Build()) - .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .Build(), }; diff --git a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs b/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs deleted file mode 100644 index e1f585806..000000000 --- a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using API.Helpers; -using Xunit; - -namespace API.Tests.Helpers; - -public class BookSortTitlePrefixHelperTests -{ - [Theory] - [InlineData("The Avengers", "Avengers")] - [InlineData("A Game of Thrones", "Game of Thrones")] - [InlineData("An American Tragedy", "American Tragedy")] - public void TestEnglishPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("El Quijote", "Quijote")] - [InlineData("La Casa de Papel", "Casa de Papel")] - [InlineData("Los Miserables", "Miserables")] - [InlineData("Las Vegas", "Vegas")] - [InlineData("Un Mundo Feliz", "Mundo Feliz")] - [InlineData("Una Historia", "Historia")] - public void TestSpanishPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Le Petit Prince", "Petit Prince")] - [InlineData("La Belle et la Bête", "Belle et la Bête")] - [InlineData("Les Misérables", "Misérables")] - [InlineData("Un Amour de Swann", "Amour de Swann")] - [InlineData("Une Vie", "Vie")] - [InlineData("Des Souris et des Hommes", "Souris et des Hommes")] - public void TestFrenchPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Der Herr der Ringe", "Herr der Ringe")] - [InlineData("Die Verwandlung", "Verwandlung")] - [InlineData("Das Kapital", "Kapital")] - [InlineData("Ein Sommernachtstraum", "Sommernachtstraum")] - [InlineData("Eine Geschichte", "Geschichte")] - public void TestGermanPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Il Nome della Rosa", "Nome della Rosa")] - [InlineData("La Divina Commedia", "Divina Commedia")] - [InlineData("Lo Hobbit", "Hobbit")] - [InlineData("Gli Ultimi", "Ultimi")] - [InlineData("Le Città Invisibili", "Città Invisibili")] - [InlineData("Un Giorno", "Giorno")] - [InlineData("Una Notte", "Notte")] - public void TestItalianPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("O Alquimista", "Alquimista")] - [InlineData("A Moreninha", "Moreninha")] - [InlineData("Os Lusíadas", "Lusíadas")] - [InlineData("As Meninas", "Meninas")] - [InlineData("Um Defeito de Cor", "Defeito de Cor")] - [InlineData("Uma História", "História")] - public void TestPortuguesePrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("", "")] // Empty string returns empty - [InlineData("Book", "Book")] // Single word, no change - [InlineData("Avengers", "Avengers")] // No prefix, no change - public void TestNoPrefixCases(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The", "The")] // Just a prefix word alone - [InlineData("A", "A")] // Just single letter prefix alone - [InlineData("Le", "Le")] // French prefix alone - public void TestPrefixWordAlone(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("THE AVENGERS", "AVENGERS")] // All caps - [InlineData("the avengers", "avengers")] // All lowercase - [InlineData("The AVENGERS", "AVENGERS")] // Mixed case - [InlineData("tHe AvEnGeRs", "AvEnGeRs")] // Random case - public void TestCaseInsensitivity(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Then Came You", "Then Came You")] // "The" + "n" = not a prefix - [InlineData("And Then There Were None", "And Then There Were None")] // "An" + "d" = not a prefix - [InlineData("Elsewhere", "Elsewhere")] // "El" + "sewhere" = not a prefix (no space) - [InlineData("Lesson Plans", "Lesson Plans")] // "Les" + "son" = not a prefix (no space) - [InlineData("Theory of Everything", "Theory of Everything")] // "The" + "ory" = not a prefix - public void TestFalsePositivePrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The ", "The ")] // Prefix with only space after - returns original - [InlineData("La ", "La ")] // Same for other languages - [InlineData("El ", "El ")] // Same for Spanish - public void TestPrefixWithOnlySpaceAfter(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The Multiple Spaces", " Multiple Spaces")] // Doesn't trim extra spaces from remainder - [InlineData("Le Petit Prince", " Petit Prince")] // Leading space preserved in remainder - public void TestSpaceHandling(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The The Matrix", "The Matrix")] // Removes first "The", leaves second - [InlineData("A A Clockwork Orange", "A Clockwork Orange")] // Removes first "A", leaves second - [InlineData("El El Cid", "El Cid")] // Spanish version - public void TestRepeatedPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("L'Étranger", "L'Étranger")] // French contraction - no space, no change - [InlineData("D'Artagnan", "D'Artagnan")] // Contraction - no space, no change - [InlineData("The-Matrix", "The-Matrix")] // Hyphen instead of space - no change - [InlineData("The.Avengers", "The.Avengers")] // Period instead of space - no change - public void TestNonSpaceSeparators(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("三国演义", "三国演义")] // Chinese - no processing due to CJK detection - [InlineData("한국어", "한국어")] // Korean - not in CJK range, would be processed normally - public void TestCjkLanguages(string inputString, string expected) - { - // NOTE: These don't do anything, I am waiting for user input on if these are needed - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("नमस्ते दुनिया", "नमस्ते दुनिया")] // Hindi - not CJK, processed normally - [InlineData("مرحبا بالعالم", "مرحبا بالعالم")] // Arabic - not CJK, processed normally - [InlineData("שלום עולם", "שלום עולם")] // Hebrew - not CJK, processed normally - public void TestNonLatinNonCjkScripts(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("в мире", "мире")] // Russian "в" (in) - should be removed - [InlineData("на столе", "столе")] // Russian "на" (on) - should be removed - [InlineData("с друзьями", "друзьями")] // Russian "с" (with) - should be removed - public void TestRussianPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } -} diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/API.Tests/Helpers/CacheHelperTests.cs index 3962ba2df..82f496a7b 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/API.Tests/Helpers/CacheHelperTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions.TestingHelpers; +using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Builders; @@ -10,9 +11,9 @@ using Xunit; namespace API.Tests.Helpers; -public class CacheHelperTests: AbstractFsTest +public class CacheHelperTests { - private static readonly string TestCoverImageDirectory = Root; + private const string TestCoverImageDirectory = @"c:\"; private const string TestCoverImageFile = "thumbnail.jpg"; private readonly string _testCoverPath = Path.Join(TestCoverImageDirectory, TestCoverImageFile); private const string TestCoverArchive = @"file in folder.zip"; @@ -36,29 +37,24 @@ public class CacheHelperTests: AbstractFsTest [Theory] [InlineData("", false)] + [InlineData("C:/", false)] [InlineData(null, false)] public void CoverImageExists_DoesFileExist(string coverImage, bool exists) { Assert.Equal(exists, _cacheHelper.CoverImageExists(coverImage)); } - [Fact] - public void CoverImageExists_DoesFileExistRoot() - { - Assert.False(_cacheHelper.CoverImageExists(Root)); - } - [Fact] public void CoverImageExists_FileExists() { - Assert.True(_cacheHelper.CoverImageExists(Path.Join(TestCoverImageDirectory, TestCoverArchive))); + Assert.True(_cacheHelper.CoverImageExists(TestCoverArchive)); } [Fact] public void ShouldUpdateCoverImage_OnFirstRun() { - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now) .Build(); Assert.True(_cacheHelper.ShouldUpdateCoverImage(null, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)), @@ -69,7 +65,7 @@ public class CacheHelperTests: AbstractFsTest public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked() { // Represents first run - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now) .Build(); Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)), @@ -80,7 +76,7 @@ public class CacheHelperTests: AbstractFsTest public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked_2() { // Represents first run - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now) .Build(); Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now, @@ -91,7 +87,7 @@ public class CacheHelperTests: AbstractFsTest public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked() { // Represents first run - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now) .Build(); Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)), @@ -102,7 +98,7 @@ public class CacheHelperTests: AbstractFsTest public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked_Modified() { // Represents first run - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now) .Build(); Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)), @@ -126,7 +122,7 @@ public class CacheHelperTests: AbstractFsTest var cacheHelper = new CacheHelper(fileService); var created = DateTime.Now.Subtract(TimeSpan.FromHours(1)); - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) .WithLastModified(DateTime.Now.Subtract(TimeSpan.FromMinutes(1))) .Build(); @@ -137,10 +133,9 @@ public class CacheHelperTests: AbstractFsTest [Fact] public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceCreated() { - var now = DateTimeOffset.Now; var filesystemFile = new MockFileData("") { - LastWriteTime =now, + LastWriteTime = DateTimeOffset.Now }; var fileSystem = new MockFileSystem(new Dictionary { @@ -152,12 +147,12 @@ public class CacheHelperTests: AbstractFsTest var cacheHelper = new CacheHelper(fileService); var chapter = new ChapterBuilder("1") - .WithLastModified(now.DateTime) - .WithCreated(now.DateTime) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) + .WithCreated(filesystemFile.LastWriteTime.DateTime) .Build(); - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) - .WithLastModified(now.DateTime) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) .Build(); Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file)); } @@ -165,10 +160,9 @@ public class CacheHelperTests: AbstractFsTest [Fact] public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified() { - var now = DateTimeOffset.Now; var filesystemFile = new MockFileData("") { - LastWriteTime = now, + LastWriteTime = DateTimeOffset.Now }; var fileSystem = new MockFileSystem(new Dictionary { @@ -180,12 +174,12 @@ public class CacheHelperTests: AbstractFsTest var cacheHelper = new CacheHelper(fileService); var chapter = new ChapterBuilder("1") - .WithLastModified(now.DateTime) - .WithCreated(now.DateTime) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) + .WithCreated(filesystemFile.LastWriteTime.DateTime) .Build(); - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) - .WithLastModified(now.DateTime) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) .Build(); Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file)); @@ -194,10 +188,9 @@ public class CacheHelperTests: AbstractFsTest [Fact] public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified_ForceUpdate() { - var now = DateTimeOffset.Now; var filesystemFile = new MockFileData("") { - LastWriteTime = now.DateTime, + LastWriteTime = DateTimeOffset.Now }; var fileSystem = new MockFileSystem(new Dictionary { @@ -209,12 +202,12 @@ public class CacheHelperTests: AbstractFsTest var cacheHelper = new CacheHelper(fileService); var chapter = new ChapterBuilder("1") - .WithLastModified(now.DateTime) - .WithCreated(now.DateTime) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) + .WithCreated(filesystemFile.LastWriteTime.DateTime) .Build(); - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) - .WithLastModified(now.DateTime) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) .Build(); Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file)); } @@ -222,11 +215,10 @@ public class CacheHelperTests: AbstractFsTest [Fact] public void IsFileUnmodifiedSinceCreationOrLastScan_ModifiedSinceLastScan() { - var now = DateTimeOffset.Now; var filesystemFile = new MockFileData("") { - LastWriteTime = now.DateTime, - CreationTime = now.DateTime + LastWriteTime = DateTimeOffset.Now, + CreationTime = DateTimeOffset.Now }; var fileSystem = new MockFileSystem(new Dictionary { @@ -242,8 +234,8 @@ public class CacheHelperTests: AbstractFsTest .WithCreated(DateTime.Now.Subtract(TimeSpan.FromMinutes(10))) .Build(); - var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) - .WithLastModified(now.DateTime) + var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) .Build(); Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file)); } @@ -251,10 +243,9 @@ public class CacheHelperTests: AbstractFsTest [Fact] public void HasFileNotChangedSinceCreationOrLastScan_ModifiedSinceLastScan_ButLastModifiedSame() { - var now = DateTimeOffset.Now; var filesystemFile = new MockFileData("") { - LastWriteTime =now.DateTime + LastWriteTime = DateTimeOffset.Now }; var fileSystem = new MockFileSystem(new Dictionary { @@ -271,7 +262,7 @@ public class CacheHelperTests: AbstractFsTest .Build(); var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive) - .WithLastModified(now.DateTime) + .WithLastModified(filesystemFile.LastWriteTime.DateTime) .Build(); Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file)); diff --git a/API.Tests/Helpers/GenreHelperTests.cs b/API.Tests/Helpers/GenreHelperTests.cs new file mode 100644 index 000000000..830f32ee0 --- /dev/null +++ b/API.Tests/Helpers/GenreHelperTests.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using API.Data; +using API.Entities; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; + +namespace API.Tests.Helpers; + +public class GenreHelperTests +{ + [Fact] + public void UpdateGenre_ShouldAddNewGenre() + { + var allGenres = new List + { + new GenreBuilder("Action").Build(), + new GenreBuilder("action").Build(), + new GenreBuilder("Sci-fi").Build(), + }; + var genreAdded = new List(); + + GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Adventure"}, genre => + { + genreAdded.Add(genre); + }); + + Assert.Equal(2, genreAdded.Count); + Assert.Equal(4, allGenres.Count); + } + + [Fact] + public void UpdateGenre_ShouldNotAddDuplicateGenre() + { + var allGenres = new List + { + new GenreBuilder("Action").Build(), + new GenreBuilder("action").Build(), + new GenreBuilder("Sci-fi").Build(), + + }; + var genreAdded = new List(); + + GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Scifi"}, genre => + { + genreAdded.Add(genre); + }); + + Assert.Equal(3, allGenres.Count); + Assert.Equal(2, genreAdded.Count); + } + + [Fact] + public void AddGenre_ShouldAddOnlyNonExistingGenre() + { + var existingGenres = new List + { + new GenreBuilder("Action").Build(), + new GenreBuilder("action").Build(), + new GenreBuilder("Sci-fi").Build(), + }; + + + GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("Action").Build()); + Assert.Equal(3, existingGenres.Count); + + GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("action").Build()); + Assert.Equal(3, existingGenres.Count); + + GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("Shonen").Build()); + Assert.Equal(4, existingGenres.Count); + } + + [Fact] + public void KeepOnlySamePeopleBetweenLists() + { + var existingGenres = new List + { + new GenreBuilder("Action").Build(), + new GenreBuilder("Sci-fi").Build(), + }; + + var peopleFromChapters = new List + { + new GenreBuilder("Action").Build(), + }; + + var genreRemoved = new List(); + GenreHelper.KeepOnlySameGenreBetweenLists(existingGenres, + peopleFromChapters, genre => + { + genreRemoved.Add(genre); + }); + + Assert.Single(genreRemoved); + } + + [Fact] + public void RemoveEveryoneIfNothingInRemoveAllExcept() + { + var existingGenres = new List + { + new GenreBuilder("Action").Build(), + new GenreBuilder("Sci-fi").Build(), + }; + + var peopleFromChapters = new List(); + + var genreRemoved = new List(); + GenreHelper.KeepOnlySameGenreBetweenLists(existingGenres, + peopleFromChapters, genre => + { + genreRemoved.Add(genre); + }); + + Assert.Equal(2, genreRemoved.Count); + } +} diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/API.Tests/Helpers/KoreaderHelperTests.cs deleted file mode 100644 index 66d287a5d..000000000 --- a/API.Tests/Helpers/KoreaderHelperTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Helpers; -using System.Runtime.CompilerServices; -using Xunit; - -namespace API.Tests.Helpers; - - -public class KoreaderHelperTests -{ - - [Theory] - [InlineData("/body/DocFragment[11]/body/div/a", 10, null)] - [InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)] - [InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)] - public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber) - { - var expected = EmptyProgressDto(); - expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[{pNumber}]" : null; - expected.PageNum = page; - var actual = EmptyProgressDto(); - - KoreaderHelper.UpdateProgressDto(actual, koreaderPosition); - Assert.Equal(expected.BookScrollId, actual.BookScrollId); - Assert.Equal(expected.PageNum, actual.PageNum); - } - - - [Theory] - [InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/div/p[20]")] - [InlineData(null, 10, "/body/DocFragment[11]/body/div/a")] - public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition) - { - var given = EmptyProgressDto(); - given.BookScrollId = scrollId; - given.PageNum = page; - - Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given)); - } - - [Theory] - [InlineData("./Data/AesopsFables.epub", "8795ACA4BF264B57C1EEDF06A0CEE688")] - public void GetKoreaderHash(string filePath, string hash) - { - Assert.Equal(KoreaderHelper.HashContents(filePath), hash); - } - - private ProgressDto EmptyProgressDto() - { - return new ProgressDto - { - ChapterId = 0, - PageNum = 0, - VolumeId = 0, - SeriesId = 0, - LibraryId = 0 - }; - } -} diff --git a/API.Tests/Helpers/OrderableHelperTests.cs b/API.Tests/Helpers/OrderableHelperTests.cs index 15f9e6268..a6d741be1 100644 --- a/API.Tests/Helpers/OrderableHelperTests.cs +++ b/API.Tests/Helpers/OrderableHelperTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using API.Entities; using API.Helpers; @@ -50,14 +49,17 @@ public class OrderableHelperTests [Fact] public void ReorderItems_InvalidPosition_NoChange() { + // Arrange var items = new List { new AppUserSideNavStream { Id = 1, Order = 0, Name = "A" }, new AppUserSideNavStream { Id = 2, Order = 1, Name = "A" }, }; + // Act OrderableHelper.ReorderItems(items, 2, 3); // Position 3 is out of range + // Assert Assert.Equal(1, items[0].Id); // Item 1 should remain at position 0 Assert.Equal(2, items[1].Id); // Item 2 should remain at position 1 } @@ -78,6 +80,7 @@ public class OrderableHelperTests [Fact] public void ReorderItems_DoubleMove() { + // Arrange var items = new List { new AppUserSideNavStream { Id = 1, Order = 0, Name = "0" }, @@ -91,6 +94,7 @@ public class OrderableHelperTests // Move 4 -> 1 OrderableHelper.ReorderItems(items, 5, 1); + // Assert Assert.Equal(1, items[0].Id); Assert.Equal(0, items[0].Order); Assert.Equal(5, items[1].Id); @@ -105,98 +109,4 @@ public class OrderableHelperTests Assert.Equal("034125", string.Join("", items.Select(s => s.Name))); } - - private static List CreateTestReadingListItems(int count = 4) - { - var items = new List(); - - for (var i = 0; i < count; i++) - { - items.Add(new ReadingListItem() { Id = i + 1, Order = count, ReadingListId = i + 1}); - } - - return items; - } - - [Fact] - public void ReorderItems_MoveItemToBeginning_CorrectOrder() - { - var items = CreateTestReadingListItems(); - - OrderableHelper.ReorderItems(items, 3, 0); - - Assert.Equal(3, items[0].Id); - Assert.Equal(1, items[1].Id); - Assert.Equal(2, items[2].Id); - Assert.Equal(4, items[3].Id); - - for (var i = 0; i < items.Count; i++) - { - Assert.Equal(i, items[i].Order); - } - } - - [Fact] - public void ReorderItems_MoveItemToEnd_CorrectOrder() - { - var items = CreateTestReadingListItems(); - - OrderableHelper.ReorderItems(items, 1, 3); - - Assert.Equal(2, items[0].Id); - Assert.Equal(3, items[1].Id); - Assert.Equal(4, items[2].Id); - Assert.Equal(1, items[3].Id); - - for (var i = 0; i < items.Count; i++) - { - Assert.Equal(i, items[i].Order); - } - } - - [Fact] - public void ReorderItems_MoveItemToMiddle_CorrectOrder() - { - var items = CreateTestReadingListItems(); - - OrderableHelper.ReorderItems(items, 4, 2); - - Assert.Equal(1, items[0].Id); - Assert.Equal(2, items[1].Id); - Assert.Equal(4, items[2].Id); - Assert.Equal(3, items[3].Id); - - for (var i = 0; i < items.Count; i++) - { - Assert.Equal(i, items[i].Order); - } - } - - [Fact] - public void ReorderItems_MoveItemToOutOfBoundsPosition_MovesToEnd() - { - var items = CreateTestReadingListItems(); - - OrderableHelper.ReorderItems(items, 2, 10); - - Assert.Equal(1, items[0].Id); - Assert.Equal(3, items[1].Id); - Assert.Equal(4, items[2].Id); - Assert.Equal(2, items[3].Id); - - for (var i = 0; i < items.Count; i++) - { - Assert.Equal(i, items[i].Order); - } - } - - [Fact] - public void ReorderItems_NegativePosition_ThrowsArgumentException() - { - var items = CreateTestReadingListItems(); - - Assert.Throws(() => - OrderableHelper.ReorderItems(items, 2, -1) - ); - } } diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs index 0bb7efb9b..70ce3aa69 100644 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ b/API.Tests/Helpers/ParserInfoHelperTests.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Tasks.Scanner; diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index 47dab48da..ed59a958f 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Builders; @@ -8,219 +11,405 @@ using Xunit; namespace API.Tests.Helpers; -public class PersonHelperTests : AbstractDbTest +public class PersonHelperTests { - protected override async Task ResetDb() - { - Context.Series.RemoveRange(Context.Series.ToList()); - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); - Context.Series.RemoveRange(Context.Series.ToList()); - await Context.SaveChangesAsync(); - } - - // 1. Test adding new people and keeping existing ones + #region UpdatePeople [Fact] - public async Task UpdateChapterPeopleAsync_AddNewPeople_ExistingPersonRetained() + public void UpdatePeople_ShouldAddNewPeople() { - await ResetDb(); + var allPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + }; + var peopleAdded = new List(); - var library = new LibraryBuilder("My Library") - .Build(); + PersonHelper.UpdatePeople(allPeople, new[] {"Joseph Shmo", "Sally Ann"}, PersonRole.Writer, person => + { + peopleAdded.Add(person); + }); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").Build(); - - // Create an existing person and assign them to the series with a role - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(existingPerson, PersonRole.Editor) - .Build()) - .WithVolume(new VolumeBuilder("1").WithChapter(chapter).Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with one existing and one new person - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo", "New Person" }, PersonRole.Editor, UnitOfWork); - - // Assert existing person retained and new person added - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Contains(people, p => p.Name == "Joe Shmo"); - Assert.Contains(people, p => p.Name == "New Person"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.Contains("New Person", chapterPeople); - } - - // 2. Test removing a person no longer in the list - [Fact] - public async Task UpdateChapterPeopleAsync_RemovePeople() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson1 = new PersonBuilder("Joe Shmo").Build(); - var existingPerson2 = new PersonBuilder("Jane Doe").Build(); - var chapter = new ChapterBuilder("1") - .WithPerson(existingPerson1, PersonRole.Editor) - .WithPerson(existingPerson2, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with only one person - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - // PersonHelper does not remove the Person from the global DbSet itself - await UnitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.DoesNotContain(people, p => p.Name == "Jane Doe"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.DoesNotContain("Jane Doe", chapterPeople); - } - - // 3. Test no changes when the list of people is the same - [Fact] - public async Task UpdateChapterPeopleAsync_NoChanges() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").WithPerson(existingPerson, PersonRole.Editor).Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with the same list - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Contains(people, p => p.Name == "Joe Shmo"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.Single(chapter.People); // No duplicate entries - } - - // 4. Test multiple roles for a person - [Fact] - public async Task UpdateChapterPeopleAsync_MultipleRoles() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var person = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").WithPerson(person, PersonRole.Writer).Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Add same person as Editor - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - // Ensure that the same person is assigned with two roles - var chapterPeople = chapter - .People - .Where(cp => - cp.Person.Name == "Joe Shmo") - .ToList(); - Assert.Equal(2, chapterPeople.Count); // One for each role - Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Writer); - Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Editor); + Assert.Equal(2, peopleAdded.Count); + Assert.Equal(4, allPeople.Count); } [Fact] - public async Task UpdateChapterPeopleAsync_MatchOnAlias_NoChanges() + public void UpdatePeople_ShouldNotAddDuplicatePeople() { - await ResetDb(); + var allPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + new PersonBuilder("Sally Ann", PersonRole.CoverArtist).Build(), - var library = new LibraryBuilder("My Library") - .Build(); + }; + var peopleAdded = new List(); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); + PersonHelper.UpdatePeople(allPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.CoverArtist, person => + { + peopleAdded.Add(person); + }); - var person = new PersonBuilder("Joe Doe") - .WithAlias("Jonny Doe") - .Build(); + Assert.Equal(3, allPeople.Count); + } + #endregion - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .Build(); + #region UpdatePeopleList - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); + [Fact] + public void UpdatePeopleList_NullTags_NoChanges() + { + // Arrange + ICollection tags = null; + var series = new SeriesBuilder("Test Series").Build(); + var allTags = new List(); + var handleAddCalled = false; + var onModifiedCalled = false; - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); + // Act + PersonHelper.UpdatePeopleList(PersonRole.Writer, tags, series, allTags, p => handleAddCalled = true, () => onModifiedCalled = true); - // Add on Name - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Doe" }, PersonRole.Editor, UnitOfWork); - await UnitOfWork.CommitAsync(); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - // Add on alias - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Jonny Doe" }, PersonRole.Editor, UnitOfWork); - await UnitOfWork.CommitAsync(); - - allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); + // Assert + Assert.False(handleAddCalled); + Assert.False(onModifiedCalled); } - // TODO: Unit tests for series + [Fact] + public void UpdatePeopleList_AddNewTag_TagAddedAndOnModifiedCalled() + { + // Arrange + const PersonRole role = PersonRole.Writer; + var tags = new List + { + new PersonDto { Id = 1, Name = "John Doe", Role = role } + }; + var series = new SeriesBuilder("Test Series").Build(); + var allTags = new List(); + var handleAddCalled = false; + var onModifiedCalled = false; + + // Act + PersonHelper.UpdatePeopleList(role, tags, series, allTags, p => + { + handleAddCalled = true; + series.Metadata.People.Add(p); + }, () => onModifiedCalled = true); + + // Assert + Assert.True(handleAddCalled); + Assert.True(onModifiedCalled); + Assert.Single(series.Metadata.People); + Assert.Equal("John Doe", series.Metadata.People.First().Name); + } + + [Fact] + public void UpdatePeopleList_RemoveExistingTag_TagRemovedAndOnModifiedCalled() + { + // Arrange + const PersonRole role = PersonRole.Writer; + var tags = new List(); + var series = new SeriesBuilder("Test Series").Build(); + var person = new PersonBuilder("John Doe", role).Build(); + person.Id = 1; + series.Metadata.People.Add(person); + var allTags = new List + { + person + }; + var handleAddCalled = false; + var onModifiedCalled = false; + + // Act + PersonHelper.UpdatePeopleList(role, tags, series, allTags, p => + { + handleAddCalled = true; + series.Metadata.People.Add(p); + }, () => onModifiedCalled = true); + + // Assert + Assert.False(handleAddCalled); + Assert.True(onModifiedCalled); + Assert.Empty(series.Metadata.People); + } + + [Fact] + public void UpdatePeopleList_UpdateExistingTag_OnModifiedCalled() + { + // Arrange + const PersonRole role = PersonRole.Writer; + var tags = new List + { + new PersonDto { Id = 1, Name = "John Doe", Role = role } + }; + var series = new SeriesBuilder("Test Series").Build(); + var person = new PersonBuilder("John Doe", role).Build(); + person.Id = 1; + series.Metadata.People.Add(person); + var allTags = new List + { + person + }; + var handleAddCalled = false; + var onModifiedCalled = false; + + // Act + PersonHelper.UpdatePeopleList(role, tags, series, allTags, p => + { + handleAddCalled = true; + series.Metadata.People.Add(p); + }, () => onModifiedCalled = true); + + // Assert + Assert.False(handleAddCalled); + Assert.False(onModifiedCalled); + Assert.Single(series.Metadata.People); + Assert.Equal("John Doe", series.Metadata.People.First().Name); + } + + [Fact] + public void UpdatePeopleList_NoChanges_HandleAddAndOnModifiedNotCalled() + { + // Arrange + const PersonRole role = PersonRole.Writer; + var tags = new List + { + new PersonDto { Id = 1, Name = "John Doe", Role = role } + }; + var series = new SeriesBuilder("Test Series").Build(); + var person = new PersonBuilder("John Doe", role).Build(); + person.Id = 1; + series.Metadata.People.Add(person); + var allTags = new List + { + new PersonBuilder("John Doe", role).Build() + }; + var handleAddCalled = false; + var onModifiedCalled = false; + + // Act + PersonHelper.UpdatePeopleList(role, tags, series, allTags, p => + { + handleAddCalled = true; + series.Metadata.People.Add(p); + }, () => onModifiedCalled = true); + + // Assert + Assert.False(handleAddCalled); + Assert.False(onModifiedCalled); + Assert.Single(series.Metadata.People); + Assert.Equal("John Doe", series.Metadata.People.First().Name); + } + + + + #endregion + + #region RemovePeople + [Fact] + public void RemovePeople_ShouldRemovePeopleOfSameRole() + { + var existingPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + }; + var peopleRemoved = new List(); + PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person => + { + peopleRemoved.Add(person); + }); + + Assert.NotEqual(existingPeople, peopleRemoved); + Assert.Single(peopleRemoved); + } + + [Fact] + public void RemovePeople_ShouldRemovePeopleFromBothRoles() + { + var existingPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + }; + var peopleRemoved = new List(); + PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person => + { + peopleRemoved.Add(person); + }); + + Assert.NotEqual(existingPeople, peopleRemoved); + Assert.Single(peopleRemoved); + + PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo"}, PersonRole.CoverArtist, person => + { + peopleRemoved.Add(person); + }); + + Assert.Empty(existingPeople); + Assert.Equal(2, peopleRemoved.Count); + } + + [Fact] + public void RemovePeople_ShouldRemovePeopleOfSameRole_WhenNothingPassed() + { + var existingPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + }; + var peopleRemoved = new List(); + PersonHelper.RemovePeople(existingPeople, new List(), PersonRole.Writer, person => + { + peopleRemoved.Add(person); + }); + + Assert.NotEqual(existingPeople, peopleRemoved); + Assert.Equal(2, peopleRemoved.Count); + } + + + #endregion + + #region KeepOnlySamePeopleBetweenLists + [Fact] + public void KeepOnlySamePeopleBetweenLists() + { + var existingPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + new PersonBuilder("Sally", PersonRole.Writer).Build(), + }; + + var peopleFromChapters = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + }; + + var peopleRemoved = new List(); + PersonHelper.KeepOnlySamePeopleBetweenLists(existingPeople, + peopleFromChapters, person => + { + peopleRemoved.Add(person); + }); + + Assert.Equal(2, peopleRemoved.Count); + } + #endregion + + #region AddPeople + + [Fact] + public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonDoesNotExist() + { + // Arrange + var metadataPeople = new List(); + var person = new PersonBuilder("John Smith", PersonRole.Character).Build(); + + // Act + PersonHelper.AddPersonIfNotExists(metadataPeople, person); + + // Assert + Assert.Single(metadataPeople); + Assert.Contains(person, metadataPeople); + } + + [Fact] + public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonAlreadyExists() + { + // Arrange + var metadataPeople = new List + { + new PersonBuilder("John Smith", PersonRole.Character) + .WithId(1) + .Build() + }; + var person = new PersonBuilder("John Smith", PersonRole.Character).Build(); + // Act + PersonHelper.AddPersonIfNotExists(metadataPeople, person); + + // Assert + Assert.Single(metadataPeople); + Assert.NotNull(metadataPeople.SingleOrDefault(p => + p.Name.Equals(person.Name) && p.Role == person.Role && p.NormalizedName == person.NormalizedName)); + Assert.Equal(1, metadataPeople.First().Id); + } + + [Fact] + public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonNameIsNullOrEmpty() + { + // Arrange + var metadataPeople = new List(); + var person2 = new PersonBuilder(string.Empty, PersonRole.Character).Build(); + + // Act + PersonHelper.AddPersonIfNotExists(metadataPeople, person2); + + // Assert + Assert.Empty(metadataPeople); + } + + [Fact] + public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsDifferentButRoleIsSame() + { + // Arrange + var metadataPeople = new List + { + new PersonBuilder("John Smith", PersonRole.Character).Build() + }; + var person = new PersonBuilder("John Doe", PersonRole.Character).Build(); + + // Act + PersonHelper.AddPersonIfNotExists(metadataPeople, person); + + // Assert + Assert.Equal(2, metadataPeople.Count); + Assert.Contains(person, metadataPeople); + } + + [Fact] + public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsSameButRoleIsDifferent() + { + // Arrange + var metadataPeople = new List + { + new PersonBuilder("John Doe", PersonRole.Writer).Build() + }; + var person = new PersonBuilder("John Smith", PersonRole.Character).Build(); + + // Act + PersonHelper.AddPersonIfNotExists(metadataPeople, person); + + // Assert + Assert.Equal(2, metadataPeople.Count); + Assert.Contains(person, metadataPeople); + } + + + + + [Fact] + public void AddPeople_ShouldAddOnlyNonExistingPeople() + { + var existingPeople = new List + { + new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(), + new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(), + new PersonBuilder("Sally", PersonRole.Writer).Build(), + }; + + + PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build()); + Assert.Equal(3, existingPeople.Count); + + PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.Writer).Build()); + Assert.Equal(3, existingPeople.Count); + + PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo Two", PersonRole.CoverArtist).Build()); + Assert.Equal(4, existingPeople.Count); + } + + #endregion + } diff --git a/API.Tests/Helpers/RandfHelper.cs b/API.Tests/Helpers/RandfHelper.cs deleted file mode 100644 index d8c007df7..000000000 --- a/API.Tests/Helpers/RandfHelper.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace API.Tests.Helpers; - -public class RandfHelper -{ - private static readonly Random Random = new (); - - /// - /// Returns true if all simple fields are equal - /// - /// - /// - /// fields to ignore, note that the names are very weird sometimes - /// - /// - /// - public static bool AreSimpleFieldsEqual(object obj1, object obj2, IList ignoreFields) - { - if (obj1 == null || obj2 == null) - throw new ArgumentNullException("Neither object can be null."); - - Type type1 = obj1.GetType(); - Type type2 = obj2.GetType(); - - if (type1 != type2) - throw new ArgumentException("Objects must be of the same type."); - - FieldInfo[] fields = type1.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); - - foreach (var field in fields) - { - if (field.IsInitOnly) continue; - if (ignoreFields.Contains(field.Name)) continue; - - Type fieldType = field.FieldType; - - if (IsRelevantType(fieldType)) - { - object value1 = field.GetValue(obj1); - object value2 = field.GetValue(obj2); - - if (!Equals(value1, value2)) - { - throw new ArgumentException("Fields must be of the same type: " + field.Name + " was " + value1 + " and " + value2); - } - } - } - - return true; - } - - private static bool IsRelevantType(Type type) - { - return type.IsPrimitive - || type == typeof(string) - || type.IsEnum; - } - - /// - /// Sets all simple fields of the given object to a random value - /// - /// - /// Simple is, primitive, string, or enum - /// - public static void SetRandomValues(object obj) - { - if (obj == null) throw new ArgumentNullException(nameof(obj)); - - Type type = obj.GetType(); - FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - foreach (var field in fields) - { - if (field.IsInitOnly) continue; // Skip readonly fields - - object value = GenerateRandomValue(field.FieldType); - if (value != null) - { - field.SetValue(obj, value); - } - } - } - - private static object GenerateRandomValue(Type type) - { - if (type == typeof(int)) - return Random.Next(); - if (type == typeof(float)) - return (float)Random.NextDouble() * 100; - if (type == typeof(double)) - return Random.NextDouble() * 100; - if (type == typeof(bool)) - return Random.Next(2) == 1; - if (type == typeof(char)) - return (char)Random.Next('A', 'Z' + 1); - if (type == typeof(byte)) - return (byte)Random.Next(0, 256); - if (type == typeof(short)) - return (short)Random.Next(short.MinValue, short.MaxValue); - if (type == typeof(long)) - return (long)(Random.NextDouble() * long.MaxValue); - if (type == typeof(string)) - return GenerateRandomString(10); - if (type.IsEnum) - { - var values = Enum.GetValues(type); - return values.GetValue(Random.Next(values.Length)); - } - - // Unsupported type - return null; - } - - private static string GenerateRandomString(int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } -} diff --git a/API.Tests/Helpers/RateLimiterTests.cs b/API.Tests/Helpers/RateLimiterTests.cs deleted file mode 100644 index e9b0030b9..000000000 --- a/API.Tests/Helpers/RateLimiterTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Helpers; -using Xunit; - -namespace API.Tests.Helpers; - -public class RateLimiterTests -{ - [Fact] - public void AcquireTokens_Successful() - { - // Arrange - var limiter = new RateLimiter(3, TimeSpan.FromSeconds(1)); - - // Act & Assert - Assert.True(limiter.TryAcquire("test_key")); - Assert.True(limiter.TryAcquire("test_key")); - Assert.True(limiter.TryAcquire("test_key")); - } - - [Fact] - public void AcquireTokens_ExceedLimit() - { - // Arrange - var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false); - - // Act - limiter.TryAcquire("test_key"); - limiter.TryAcquire("test_key"); - - // Assert - Assert.False(limiter.TryAcquire("test_key")); - } - - [Fact] - public async Task AcquireTokens_Refill() - { - // Arrange - var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1)); - - // Act - limiter.TryAcquire("test_key"); - limiter.TryAcquire("test_key"); - - // Wait for refill - await Task.Delay(1100); - - // Assert - Assert.True(limiter.TryAcquire("test_key")); - } - - [Fact] - public async Task AcquireTokens_Refill_WithOff() - { - // Arrange - var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false); - - // Act - limiter.TryAcquire("test_key"); - limiter.TryAcquire("test_key"); - - // Wait for refill - await Task.Delay(2100); - - // Assert - Assert.False(limiter.TryAcquire("test_key")); - } - - [Fact] - public void AcquireTokens_MultipleKeys() - { - // Arrange - var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1)); - - // Act & Assert - Assert.True(limiter.TryAcquire("key1")); - Assert.True(limiter.TryAcquire("key2")); - } -} diff --git a/API.Tests/Helpers/ReviewHelperTests.cs b/API.Tests/Helpers/ReviewHelperTests.cs deleted file mode 100644 index b221c3c70..000000000 --- a/API.Tests/Helpers/ReviewHelperTests.cs +++ /dev/null @@ -1,258 +0,0 @@ -using API.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; -using API.DTOs.SeriesDetail; - -namespace API.Tests.Helpers; - -public class ReviewHelperTests -{ - #region SelectSpectrumOfReviews Tests - - [Fact] - public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews() - { - // Arrange - var reviews = CreateReviewList(8); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(8, result.Count); - Assert.Equal(reviews, result.OrderByDescending(r => r.Score)); - } - - [Fact] - public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews() - { - // Arrange - var reviews = CreateReviewList(20); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Equal(reviews[0], result.First()); - Assert.Equal(reviews[19], result.Last()); - } - - [Fact] - public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews() - { - // Arrange - var reviews = CreateReviewList(10); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - } - - [Fact] - public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum() - { - // Arrange - var reviews = CreateReviewList(100); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Contains(reviews[0], result); - Assert.Contains(reviews[1], result); - Assert.Contains(reviews[98], result); - Assert.Contains(reviews[99], result); - } - - [Fact] - public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList() - { - // Arrange - var reviews = new List(); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Empty(result); - } - - [Fact] - public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending() - { - // Arrange - var reviews = new List - { - new UserReviewDto { Tagline = "1", Score = 3 }, - new UserReviewDto { Tagline = "2", Score = 5 }, - new UserReviewDto { Tagline = "3", Score = 1 }, - new UserReviewDto { Tagline = "4", Score = 4 }, - new UserReviewDto { Tagline = "5", Score = 2 } - }; - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(5, result.Count); - Assert.Equal(5, result[0].Score); - Assert.Equal(4, result[1].Score); - Assert.Equal(3, result[2].Score); - Assert.Equal(2, result[3].Score); - Assert.Equal(1, result[4].Score); - } - - #endregion - - #region GetCharacters Tests - - [Fact] - public void GetCharacters_WithNullBody_ReturnsNull() - { - // Arrange - string body = null; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetCharacters_WithEmptyBody_ReturnsEmptyString() - { - // Arrange - var body = string.Empty; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithNoTextNodes_ReturnsEmptyString() - { - // Arrange - const string body = "
"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText() - { - // Arrange - var body = "

This is a short review.

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is a short review.…", result); - } - - [Fact] - public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText() - { - // Arrange - var body = "

" + new string('a', 200) + "

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(new string('a', 175) + "…", result); - Assert.Equal(176, result.Length); // 175 characters + ellipsis - } - - [Fact] - public void GetCharacters_IgnoresScriptTags() - { - // Arrange - const string body = "

Visible text

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("Visible text…", result); - Assert.DoesNotContain("hidden", result); - } - - [Fact] - public void GetCharacters_RemovesMarkdownSymbols() - { - // Arrange - const string body = "

This is **bold** and _italic_ text with [link](url).

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is bold and italic text with link.…", result); - } - - [Fact] - public void GetCharacters_HandlesComplexMarkdownAndHtml() - { - // Arrange - const string body = """ - -
-

# Header

-

This is ~~strikethrough~~ and __underlined__ text

-

~~~code block~~~

-

+++highlighted+++

-

img123(image.jpg)

-
- """; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.DoesNotContain("~~", result); - Assert.DoesNotContain("__", result); - Assert.DoesNotContain("~~~", result); - Assert.DoesNotContain("+++", result); - Assert.DoesNotContain("img123(", result); - Assert.Contains("Header", result); - Assert.Contains("strikethrough", result); - Assert.Contains("underlined", result); - Assert.Contains("code block", result); - Assert.Contains("highlighted", result); - } - - #endregion - - #region Helper Methods - - private static List CreateReviewList(int count) - { - var reviews = new List(); - for (var i = 0; i < count; i++) - { - reviews.Add(new UserReviewDto - { - Tagline = $"{i + 1}", - Score = count - i // This makes them ordered by score descending initially - }); - } - return reviews; - } - - #endregion -} - diff --git a/API.Tests/Helpers/ScannerHelper.cs b/API.Tests/Helpers/ScannerHelper.cs deleted file mode 100644 index 653efebb1..000000000 --- a/API.Tests/Helpers/ScannerHelper.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.IO.Compression; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using System.Xml; -using System.Xml.Serialization; -using API.Data; -using API.Data.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit.Abstractions; - -namespace API.Tests.Helpers; -#nullable enable - -public class ScannerHelper -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); - private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); - private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); - private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" }; - - public ScannerHelper(IUnitOfWork unitOfWork, ITestOutputHelper testOutputHelper) - { - _unitOfWork = unitOfWork; - _testOutputHelper = testOutputHelper; - } - - public async Task GenerateScannerData(string testcase, Dictionary comicInfos = null) - { - var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos); - - var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase)); - - var library = new LibraryBuilder(publisher, type) - .WithFolders([new FolderPath() {Path = testDirectoryPath}]) - .Build(); - - var admin = new AppUserBuilder("admin", "admin@kavita.com", Seed.DefaultThemes[0]) - .WithLibrary(library) - .Build(); - - _unitOfWork.UserRepository.Add(admin); // Admin is needed for generating collections/reading lists - _unitOfWork.LibraryRepository.Add(library); - await _unitOfWork.CommitAsync(); - - return library; - } - - public ScannerService CreateServices(DirectoryService ds = null, IFileSystem fs = null) - { - fs ??= new FileSystem(); - ds ??= new DirectoryService(Substitute.For>(), fs); - var archiveService = new ArchiveService(Substitute.For>(), ds, - Substitute.For(), Substitute.For()); - var readingItemService = new ReadingItemService(archiveService, Substitute.For(), - Substitute.For(), ds, Substitute.For>()); - - - var processSeries = new ProcessSeries(_unitOfWork, Substitute.For>(), - Substitute.For(), - ds, Substitute.For(), readingItemService, new FileService(fs), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - var scanner = new ScannerService(_unitOfWork, Substitute.For>(), - Substitute.For(), - Substitute.For(), Substitute.For(), ds, - readingItemService, processSeries, Substitute.For()); - return scanner; - } - - private static (string Publisher, LibraryType Type) SplitPublisherAndLibraryType(string input) - { - // Split the input string based on " - " - var parts = input.Split(" - ", StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length != 2) - { - throw new ArgumentException("Input must be in the format 'Publisher - LibraryType'"); - } - - var publisher = parts[0].Trim(); - var libraryTypeString = parts[1].Trim(); - - // Try to parse the right-hand side as a LibraryType enum - if (!Enum.TryParse(libraryTypeString, out var libraryType)) - { - throw new ArgumentException($"'{libraryTypeString}' is not a valid LibraryType"); - } - - return (publisher, libraryType); - } - - - - private async Task GenerateTestDirectory(string mapPath, Dictionary comicInfos = null) - { - // Read the map file - var mapContent = await File.ReadAllTextAsync(mapPath); - - // Deserialize the JSON content into a list of strings using System.Text.Json - var filePaths = JsonSerializer.Deserialize>(mapContent); - - // Create a test directory - var testDirectory = Path.Combine(_testDirectory, Path.GetFileNameWithoutExtension(mapPath)); - if (Directory.Exists(testDirectory)) - { - Directory.Delete(testDirectory, true); - } - Directory.CreateDirectory(testDirectory); - - // Generate the files and folders - await Scaffold(testDirectory, filePaths, comicInfos); - - _testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}"); - - return Path.GetFullPath(testDirectory); - } - - - public async Task Scaffold(string testDirectory, List filePaths, Dictionary comicInfos = null) - { - foreach (var relativePath in filePaths) - { - var fullPath = Path.Combine(testDirectory, relativePath); - var fileDir = Path.GetDirectoryName(fullPath); - - // Create the directory if it doesn't exist - if (!Directory.Exists(fileDir)) - { - Directory.CreateDirectory(fileDir); - Console.WriteLine($"Created directory: {fileDir}"); - } - - var ext = Path.GetExtension(fullPath).ToLower(); - if (ComicInfoExtensions.Contains(ext) && comicInfos != null && comicInfos.TryGetValue(Path.GetFileName(relativePath), out var info)) - { - CreateMinimalCbz(fullPath, info); - } - else - { - // Create an empty file - await File.Create(fullPath).DisposeAsync(); - Console.WriteLine($"Created empty file: {fullPath}"); - } - } - } - - private void CreateMinimalCbz(string filePath, ComicInfo? comicInfo = null) - { - using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Create)) - { - // Add the 1x1 image to the archive - archive.CreateEntryFromFile(_imagePath, "1x1.png"); - - if (comicInfo != null) - { - // Serialize ComicInfo object to XML - var comicInfoXml = SerializeComicInfoToXml(comicInfo); - - // Create an entry for ComicInfo.xml in the archive - var entry = archive.CreateEntry("ComicInfo.xml"); - using var entryStream = entry.Open(); - using var writer = new StreamWriter(entryStream, Encoding.UTF8); - - // Write the XML to the archive - writer.Write(comicInfoXml); - } - - } - Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata."); - } - - - private static string SerializeComicInfoToXml(ComicInfo comicInfo) - { - var xmlSerializer = new XmlSerializer(typeof(ComicInfo)); - using var stringWriter = new StringWriter(); - using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false), OmitXmlDeclaration = false})) - { - xmlSerializer.Serialize(xmlWriter, comicInfo); - } - - // For the love of god, I spent 2 hours trying to get utf-8 with no BOM - return stringWriter.ToString().Replace("""""", - @""); - } -} diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/API.Tests/Helpers/SeriesHelperTests.cs index 22b4a3cd1..a5b5a063b 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/API.Tests/Helpers/SeriesHelperTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using API.Data; using API.Entities; using API.Entities.Enums; using API.Extensions; diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/API.Tests/Helpers/SmartFilterHelperTests.cs index 974cb0ba6..510748821 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/API.Tests/Helpers/SmartFilterHelperTests.cs @@ -1,62 +1,39 @@ using System; using System.Collections.Generic; using System.Linq; -using API.Data.ManualMigrations; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.Entities.Enums; using API.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Xunit; namespace API.Tests.Helpers; public class SmartFilterHelperTests { - - [Theory] - [InlineData("", false)] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)] - [InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)] - [InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)] - public void Test_ShouldMigrateFilter(string filter, bool expected) - { - Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter)); - } - [Fact] public void Test_Decode() { - const string encoded = """ - name=Test&stmts=comparison%253D0%25C2%25A6field%253D18%25C2%25A6value%253D95�comparison%253D0%25C2%25A6field%253D4%25C2%25A6value%253D0�comparison%253D7%25C2%25A6field%253D1%25C2%25A6value%253Da&sortOptions=sortField%3D2¦isAscending%3DFalse&limitTo=10&combination=1 - """; + var encoded = """ + stmts=comparison%3D5%26field%3D18%26value%3D95%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=2&isAscending=false&limitTo=10&combination=1 + """; var filter = SmartFilterHelper.Decode(encoded); Assert.Equal(10, filter.LimitTo); Assert.Equal(SortField.CreatedDate, filter.SortOptions.SortField); Assert.False(filter.SortOptions.IsAscending); - Assert.Equal("Test" , filter.Name); + Assert.Null(filter.Name); var list = filter.Statements.ToList(); AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a"); AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + string.Empty); - AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95"); + AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "95"); } [Fact] - public void Test_Decode2() - { - const string encoded = """ - name=Test%202&stmts=comparison%253D10%25C2%25A6field%253D1%25C2%25A6value%253DA%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D11&sortOptions=sortField%3D1%C2%A6isAscending%3DTrue&limitTo=0&combination=1 - """; - - var filter = SmartFilterHelper.Decode(encoded); - Assert.True(filter.SortOptions.IsAscending); - } - - [Fact] - public void Test_EncodeDecode() + public void Test_Encode() { var filter = new FilterV2Dto() { @@ -79,79 +56,10 @@ public class SmartFilterHelperTests }; var encodedFilter = SmartFilterHelper.Encode(filter); - - var decoded = SmartFilterHelper.Decode(encodedFilter); - Assert.Single(decoded.Statements); - AssertStatementSame(decoded.Statements.First(), filter.Statements.First()); - Assert.Equal("Test", decoded.Name); - Assert.Equal(10, decoded.LimitTo); - Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField); - Assert.False(decoded.SortOptions.IsAscending); + Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%26isAscending%3DFalse&limitTo=10&combination=1", encodedFilter); } - [Fact] - public void Test_EncodeDecode_MultipleValues_Contains() - { - var filter = new FilterV2Dto() - { - Name = "Test", - SortOptions = new SortOptions() { - IsAscending = false, - SortField = SortField.CreatedDate - }, - LimitTo = 10, - Combination = FilterCombination.And, - Statements = new List() - { - new FilterStatementDto() - { - Comparison = FilterComparison.Equal, - Field = FilterField.AgeRating, - Value = $"{(int) AgeRating.Unknown + string.Empty},{(int) AgeRating.G + string.Empty}" - } - } - }; - - var encodedFilter = SmartFilterHelper.Encode(filter); - var decoded = SmartFilterHelper.Decode(encodedFilter); - - Assert.Single(decoded.Statements); - AssertStatementSame(decoded.Statements.First(), filter.Statements.First()); - - Assert.Equal(2, decoded.Statements.First().Value.Split(",").Length); - - Assert.Equal("Test", decoded.Name); - Assert.Equal(10, decoded.LimitTo); - Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField); - Assert.False(decoded.SortOptions.IsAscending); - } - - [Theory] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1")] - [InlineData("name=Manga%20-%20On%20Deck&stmts=comparison%253D1%252Cfield%253D20%252Cvalue%253D0,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D0%252Cfield%253D19%252Cvalue%253D2&sortOptions=sortField%3D1,isAscending%3DTrue&limitTo=0&combination=1")] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1")] - public void MigrationWorks(string filter) - { - try - { - var updatedFilter = MigrateSmartFilterEncoding.EncodeFix(filter); - Assert.NotNull(updatedFilter); - } - catch (Exception ex) - { - Assert.Fail("Exception thrown: " + ex.Message); - } - - } - - private static void AssertStatementSame(FilterStatementDto statement, FilterStatementDto statement2) - { - Assert.Equal(statement.Field, statement2.Field); - Assert.Equal(statement.Comparison, statement2.Comparison); - Assert.Equal(statement.Value, statement2.Value); - } - - private static void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value) + private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value) { Assert.Equal(statement.Field, field); Assert.Equal(statement.Comparison, combination); diff --git a/API.Tests/Helpers/StringHelperTests.cs b/API.Tests/Helpers/StringHelperTests.cs deleted file mode 100644 index 8f845c9b0..000000000 --- a/API.Tests/Helpers/StringHelperTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using API.Helpers; -using Xunit; - -namespace API.Tests.Helpers; - -public class StringHelperTests -{ - [Theory] - [InlineData( - "

A Perfect Marriage Becomes a Perfect Affair!



Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?

", - "

A Perfect Marriage Becomes a Perfect Affair!
Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?

" - )] - [InlineData( - "

Blog | Twitter | Pixiv | Pawoo

", - "

Blog | Twitter | Pixiv | Pawoo

" - )] - public void TestSquashBreaklines(string input, string expected) - { - Assert.Equal(expected, StringHelper.SquashBreaklines(input)); - } - - [Theory] - [InlineData( - "

A Perfect Marriage Becomes a Perfect Affair!
(Source: Anime News Network)

", - "

A Perfect Marriage Becomes a Perfect Affair!

" - )] - [InlineData( - "

A Perfect Marriage Becomes a Perfect Affair!

(Source: Anime News Network)", - "

A Perfect Marriage Becomes a Perfect Affair!

" - )] - public void TestRemoveSourceInDescription(string input, string expected) - { - Assert.Equal(expected, StringHelper.RemoveSourceInDescription(input)); - } - - - [Theory] - [InlineData( -"""Pawoo

""", -"""Pawoo

""" - )] - public void TestCorrectUrls(string input, string expected) - { - Assert.Equal(expected, StringHelper.CorrectUrls(input)); - } -} diff --git a/API.Tests/Helpers/TagHelperTests.cs b/API.Tests/Helpers/TagHelperTests.cs new file mode 100644 index 000000000..430a85d69 --- /dev/null +++ b/API.Tests/Helpers/TagHelperTests.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using API.Data; +using API.Entities; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; + +namespace API.Tests.Helpers; + +public class TagHelperTests +{ + [Fact] + public void UpdateTag_ShouldAddNewTag() + { + var allTags = new List + { + new TagBuilder("Action").Build(), + new TagBuilder("action").Build(), + new TagBuilder("Sci-fi").Build(), + }; + var tagAdded = new List(); + + TagHelper.UpdateTag(allTags, new[] {"Action", "Adventure"}, (tag, added) => + { + if (added) + { + tagAdded.Add(tag); + } + + }); + + Assert.Single(tagAdded); + Assert.Equal(4, allTags.Count); + } + + [Fact] + public void UpdateTag_ShouldNotAddDuplicateTag() + { + var allTags = new List + { + new TagBuilder("Action").Build(), + new TagBuilder("action").Build(), + new TagBuilder("Sci-fi").Build(), + + }; + var tagAdded = new List(); + + TagHelper.UpdateTag(allTags, new[] {"Action", "Scifi"}, (tag, added) => + { + if (added) + { + tagAdded.Add(tag); + } + TagHelper.AddTagIfNotExists(allTags, tag); + }); + + Assert.Equal(3, allTags.Count); + Assert.Empty(tagAdded); + } + + [Fact] + public void AddTag_ShouldAddOnlyNonExistingTag() + { + var existingTags = new List + { + new TagBuilder("Action").Build(), + new TagBuilder("action").Build(), + new TagBuilder("Sci-fi").Build(), + }; + + + TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("Action").Build()); + Assert.Equal(3, existingTags.Count); + + TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("action").Build()); + Assert.Equal(3, existingTags.Count); + + TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("Shonen").Build()); + Assert.Equal(4, existingTags.Count); + } + + [Fact] + public void KeepOnlySamePeopleBetweenLists() + { + var existingTags = new List + { + new TagBuilder("Action").Build(), + new TagBuilder("Sci-fi").Build(), + }; + + var peopleFromChapters = new List + { + new TagBuilder("Action").Build(), + }; + + var tagRemoved = new List(); + TagHelper.KeepOnlySameTagBetweenLists(existingTags, + peopleFromChapters, tag => + { + tagRemoved.Add(tag); + }); + + Assert.Single(tagRemoved); + } + + [Fact] + public void RemoveEveryoneIfNothingInRemoveAllExcept() + { + var existingTags = new List + { + new TagBuilder("Action").Build(), + new TagBuilder("Sci-fi").Build(), + }; + + var peopleFromChapters = new List(); + + var tagRemoved = new List(); + TagHelper.KeepOnlySameTagBetweenLists(existingTags, + peopleFromChapters, tag => + { + tagRemoved.Add(tag); + }); + + Assert.Equal(2, tagRemoved.Count); + } +} diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs new file mode 100644 index 000000000..52fd02ae8 --- /dev/null +++ b/API.Tests/Parser/BookParserTests.cs @@ -0,0 +1,43 @@ +using Xunit; + +namespace API.Tests.Parser; + +public class BookParserTests +{ + [Theory] + [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")] + [InlineData("BBC Focus 00 The Science of Happiness 2nd Edition (2018)", "BBC Focus 00 The Science of Happiness 2nd Edition")] + [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")] + public void ParseSeriesTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); + } + + [Theory] + [InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")] + [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")] + public void ParseVolumeTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); + } + + // [Theory] + // [InlineData("@font-face{font-family:'syyskuu_repaleinen';src:url(data:font/opentype;base64,AAEAAAA", "@font-face{font-family:'syyskuu_repaleinen';src:url(data:font/opentype;base64,AAEAAAA")] + // [InlineData("@font-face{font-family:'syyskuu_repaleinen';src:url('fonts/font.css')", "@font-face{font-family:'syyskuu_repaleinen';src:url('TEST/fonts/font.css')")] + // public void ReplaceFontSrcUrl(string input, string expected) + // { + // var apiBase = "TEST/"; + // var actual = API.Parser.Parser.FontSrcUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3"); + // Assert.Equal(expected, actual); + // } + // + // [Theory] + // [InlineData("@import url('font.css');", "@import url('TEST/font.css');")] + // public void ReplaceImportSrcUrl(string input, string expected) + // { + // var apiBase = "TEST/"; + // var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3"); + // Assert.Equal(expected, actual); + // } + +} diff --git a/API.Tests/Parsing/ComicParsingTests.cs b/API.Tests/Parser/ComicParserTests.cs similarity index 67% rename from API.Tests/Parsing/ComicParsingTests.cs rename to API.Tests/Parser/ComicParserTests.cs index a0375a566..4740c4f54 100644 --- a/API.Tests/Parsing/ComicParsingTests.cs +++ b/API.Tests/Parser/ComicParserTests.cs @@ -1,11 +1,26 @@ -using API.Entities.Enums; +using System.IO.Abstractions.TestingHelpers; +using API.Services; using API.Services.Tasks.Scanner.Parser; +using Microsoft.Extensions.Logging; +using NSubstitute; using Xunit; +using Xunit.Abstractions; -namespace API.Tests.Parsing; +namespace API.Tests.Parser; -public class ComicParsingTests +public class ComicParserTests { + private readonly ITestOutputHelper _testOutputHelper; + private readonly DefaultParser _defaultParser; + + public ComicParserTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _defaultParser = + new DefaultParser(new DirectoryService(Substitute.For>(), + new MockFileSystem())); + } + [Theory] [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")] [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")] @@ -51,58 +66,56 @@ public class ComicParsingTests [InlineData("Demon 012 (Sep 1973) c2c", "Demon")] [InlineData("Dragon Age - Until We Sleep 01 (of 03)", "Dragon Age - Until We Sleep")] [InlineData("Green Lantern v2 017 - The Spy-Eye that doomed Green Lantern v2", "Green Lantern")] - [InlineData("Green Lantern - Circle of Fire Special - Adam Strange (2000)", "Green Lantern - Circle of Fire Special - Adam Strange")] - [InlineData("Identity Crisis Extra - Rags Morales Sketches (2005)", "Identity Crisis Extra - Rags Morales Sketches")] + [InlineData("Green Lantern - Circle of Fire Special - Adam Strange (2000)", "Green Lantern - Circle of Fire - Adam Strange")] + [InlineData("Identity Crisis Extra - Rags Morales Sketches (2005)", "Identity Crisis - Rags Morales Sketches")] [InlineData("Daredevil - t6 - 10 - (2019)", "Daredevil")] [InlineData("Batgirl T2000 #57", "Batgirl")] [InlineData("Teen Titans t1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")] [InlineData("Conquistador_-Tome_2", "Conquistador")] [InlineData("Max_l_explorateur-_Tome_0", "Max l explorateur")] [InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "Chevaliers d'Héliopolis")] - [InlineData("Bd Fr-Aldebaran-Antares-t6", "Bd Fr-Aldebaran-Antares")] + [InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")] [InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")] [InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")] [InlineData("Kebab Том 1 Глава 1", "Kebab")] [InlineData("Манга Глава 1", "Манга")] - [InlineData("ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก เล่ม 1", "ReZero รีเซทชีวิต ฝ่าวิกฤตต่างโลก")] - [InlineData("SKY WORLD สกายเวิลด์ เล่มที่ 1", "SKY WORLD สกายเวิลด์")] public void ParseComicSeriesTest(string filename, string expected) { - Assert.Equal(expected, Parser.ParseComicSeries(filename)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename)); } [Theory] - [InlineData("01 Spider-Man & Wolverine 01.cbr", Parser.LooseLeafVolume)] - [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", Parser.LooseLeafVolume)] - [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", Parser.LooseLeafVolume)] - [InlineData("Batman & Catwoman - Trail of the Gun 01", Parser.LooseLeafVolume)] - [InlineData("Batman & Daredevil - King of New York", Parser.LooseLeafVolume)] - [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", Parser.LooseLeafVolume)] - [InlineData("Batman & Robin the Teen Wonder #0", Parser.LooseLeafVolume)] - [InlineData("Batman & Wildcat (1 of 3)", Parser.LooseLeafVolume)] - [InlineData("Batman And Superman World's Finest #01", Parser.LooseLeafVolume)] - [InlineData("Babe 01", Parser.LooseLeafVolume)] - [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", Parser.LooseLeafVolume)] + [InlineData("01 Spider-Man & Wolverine 01.cbr", "0")] + [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")] + [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] + [InlineData("Batman & Catwoman - Trail of the Gun 01", "0")] + [InlineData("Batman & Daredevil - King of New York", "0")] + [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "0")] + [InlineData("Batman & Robin the Teen Wonder #0", "0")] + [InlineData("Batman & Wildcat (1 of 3)", "0")] + [InlineData("Batman And Superman World's Finest #01", "0")] + [InlineData("Babe 01", "0")] + [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "0")] [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")] - [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", Parser.LooseLeafVolume)] + [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "0")] [InlineData("Superman v1 024 (09-10 1943)", "1")] [InlineData("Superman v1.5 024 (09-10 1943)", "1.5")] - [InlineData("Amazing Man Comics chapter 25", Parser.LooseLeafVolume)] - [InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", Parser.LooseLeafVolume)] - [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", Parser.LooseLeafVolume)] - [InlineData("spawn-123", Parser.LooseLeafVolume)] - [InlineData("spawn-chapter-123", Parser.LooseLeafVolume)] - [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", Parser.LooseLeafVolume)] - [InlineData("Batman Beyond 04 (of 6) (1999)", Parser.LooseLeafVolume)] - [InlineData("Batman Beyond 001 (2012)", Parser.LooseLeafVolume)] - [InlineData("Batman Beyond 2.0 001 (2013)", Parser.LooseLeafVolume)] - [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", Parser.LooseLeafVolume)] + [InlineData("Amazing Man Comics chapter 25", "0")] + [InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "0")] + [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")] + [InlineData("spawn-123", "0")] + [InlineData("spawn-chapter-123", "0")] + [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "0")] + [InlineData("Batman Beyond 04 (of 6) (1999)", "0")] + [InlineData("Batman Beyond 001 (2012)", "0")] + [InlineData("Batman Beyond 2.0 001 (2013)", "0")] + [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "0")] [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "1")] - [InlineData("Chew Script Book (2011) (digital-Empire) SP04", Parser.LooseLeafVolume)] + [InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")] [InlineData("Batgirl Vol.2000 #57 (December, 2004)", "2000")] [InlineData("Batgirl V2000 #57", "2000")] - [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", Parser.LooseLeafVolume)] - [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", Parser.LooseLeafVolume)] + [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "0")] + [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", "0")] [InlineData("Daredevil - v6 - 10 - (2019)", "6")] [InlineData("Daredevil - v6.5", "6.5")] // Tome Tests @@ -112,25 +125,22 @@ public class ComicParsingTests [InlineData("Conquistador_Tome_2", "2")] [InlineData("Max_l_explorateur-_Tome_0", "0")] [InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")] - [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", Parser.LooseLeafVolume)] + [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "0")] [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")] // Russian Tests [InlineData("Kebab Том 1 Глава 3", "1")] - [InlineData("Манга Глава 2", Parser.LooseLeafVolume)] - [InlineData("ย้อนเวลากลับมาร้าย เล่ม 1", "1")] - [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "1")] - [InlineData("วิวาห์รัก เดิมพันชีวิต ตอนที่ 2", Parser.LooseLeafVolume)] + [InlineData("Манга Глава 2", "0")] public void ParseComicVolumeTest(string filename, string expected) { - Assert.Equal(expected, Parser.ParseComicVolume(filename)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename)); } [Theory] [InlineData("01 Spider-Man & Wolverine 01.cbr", "1")] - [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", Parser.DefaultChapter)] - [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", Parser.DefaultChapter)] + [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")] + [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] [InlineData("Batman & Catwoman - Trail of the Gun 01", "1")] - [InlineData("Batman & Daredevil - King of New York", Parser.DefaultChapter)] + [InlineData("Batman & Daredevil - King of New York", "0")] [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")] [InlineData("Batman & Robin the Teen Wonder #0", "0")] [InlineData("Batman & Wildcat (1 of 3)", "1")] @@ -154,8 +164,8 @@ public class ComicParsingTests [InlineData("Batman Beyond 001 (2012)", "1")] [InlineData("Batman Beyond 2.0 001 (2013)", "1")] [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "1")] - [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", Parser.DefaultChapter)] - [InlineData("Chew Script Book (2011) (digital-Empire) SP04", Parser.DefaultChapter)] + [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "0")] + [InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")] [InlineData("Batgirl Vol.2000 #57 (December, 2004)", "57")] [InlineData("Batgirl V2000 #57", "57")] [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "21")] @@ -164,47 +174,43 @@ public class ComicParsingTests [InlineData("Daredevil - v6 - 10 - (2019)", "10")] [InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")] [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")] - [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", Parser.DefaultChapter)] + [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "0")] [InlineData("Kebab Том 1 Глава 3", "3")] [InlineData("Манга Глава 2", "2")] [InlineData("Манга 2 Глава", "2")] [InlineData("Манга Том 1 2 Глава", "2")] - [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")] - [InlineData("Max Level Returner ตอนที่ 5", "5")] - [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] public void ParseComicChapterTest(string filename, string expected) { - Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Comic)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename)); } [Theory] - [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", false)] - [InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", false)] + [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", true)] + [InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", true)] [InlineData("Baldwin the Brave & Other Tales Special SP1.cbr", true)] - [InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", false)] - [InlineData("Boule et Bill - THS -Bill à disparu", false)] - [InlineData("Asterix - HS - Les 12 travaux d'Astérix", false)] - [InlineData("Sillage Hors Série - Le Collectionneur - Concordance-DKFR", false)] + [InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", true)] + [InlineData("Boule et Bill - THS -Bill à disparu", true)] + [InlineData("Asterix - HS - Les 12 travaux d'Astérix", true)] + [InlineData("Sillage Hors Série - Le Collectionneur - Concordance-DKFR", true)] [InlineData("laughs", false)] - [InlineData("Annual Days of Summer", false)] - [InlineData("Adventure Time 2013 Annual #001 (2013)", false)] - [InlineData("Adventure Time 2013_Annual_#001 (2013)", false)] - [InlineData("Adventure Time 2013_-_Annual #001 (2013)", false)] + [InlineData("Annual Days of Summer", true)] + [InlineData("Adventure Time 2013 Annual #001 (2013)", true)] + [InlineData("Adventure Time 2013_Annual_#001 (2013)", true)] + [InlineData("Adventure Time 2013_-_Annual #001 (2013)", true)] [InlineData("G.I. Joe - A Real American Hero Yearbook 004 Reprint (2021)", false)] [InlineData("Mazebook 001", false)] - [InlineData("X-23 One Shot (2010)", false)] - [InlineData("Casus Belli v1 Hors-Série 21 - Mousquetaires et Sorcellerie", false)] - [InlineData("Batman Beyond Annual", false)] - [InlineData("Batman Beyond Bonus", false)] - [InlineData("Batman Beyond OneShot", false)] - [InlineData("Batman Beyond Specials", false)] - [InlineData("Batman Beyond Omnibus (1999)", false)] - [InlineData("Batman Beyond Omnibus", false)] - [InlineData("01 Annual Batman Beyond", false)] - [InlineData("Blood Syndicate Annual #001", false)] + [InlineData("X-23 One Shot (2010)", true)] + [InlineData("Casus Belli v1 Hors-Série 21 - Mousquetaires et Sorcellerie", true)] + [InlineData("Batman Beyond Annual", true)] + [InlineData("Batman Beyond Bonus", true)] + [InlineData("Batman Beyond OneShot", true)] + [InlineData("Batman Beyond Specials", true)] + [InlineData("Batman Beyond Omnibus (1999)", true)] + [InlineData("Batman Beyond Omnibus", true)] + [InlineData("01 Annual Batman Beyond", true)] public void IsComicSpecialTest(string input, bool expected) { - Assert.Equal(expected, Parser.IsSpecial(input, LibraryType.Comic)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsComicSpecial(input)); } } diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs similarity index 62% rename from API.Tests/Parsers/DefaultParserTests.cs rename to API.Tests/Parser/DefaultParserTests.cs index 244c08b97..61ed57aca 100644 --- a/API.Tests/Parsers/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO.Abstractions.TestingHelpers; +using System.Linq; using API.Entities.Enums; using API.Services; using API.Services.Tasks.Scanner.Parser; @@ -8,7 +10,7 @@ using NSubstitute; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Parsers; +namespace API.Tests.Parser; public class DefaultParserTests { @@ -19,12 +21,10 @@ public class DefaultParserTests { _testOutputHelper = testOutputHelper; var directoryService = new DirectoryService(Substitute.For>(), new MockFileSystem()); - _defaultParser = new BasicParser(directoryService, new ImageParser(directoryService)); + _defaultParser = new DefaultParser(directoryService); } - - #region ParseFromFallbackFolders [Theory] [InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")] @@ -33,7 +33,7 @@ public class DefaultParserTests [InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")] public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries) { - var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, true, null); + var actual = _defaultParser.Parse(inputPath, rootDir); if (actual == null) { Assert.NotNull(actual); @@ -44,24 +44,25 @@ public class DefaultParserTests } [Theory] - [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", new [] {"Btooom!", "1", "1"})] - [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", new [] {"Btooom!", "1", "2"})] - [InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", new [] {"Monster", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, "1"})] - [InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", new [] {"Hajime no Ippo", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter})] - public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo) + [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")] + [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!~1~2")] + [InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster~0~1")] + [InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", "Hajime no Ippo~0~0")] + public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string expectedParseInfo) { const string rootDirectory = "/manga/"; - var actual = new ParserInfo {Series = "", Chapters = Parser.DefaultChapter, Volumes = Parser.LooseLeafVolume}; + var tokens = expectedParseInfo.Split("~"); + var actual = new ParserInfo {Series = "", Chapters = "0", Volumes = "0"}; _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); - Assert.Equal(expectedParseInfo[0], actual.Series); - Assert.Equal(expectedParseInfo[1], actual.Volumes); - Assert.Equal(expectedParseInfo[2], actual.Chapters); + Assert.Equal(tokens[0], actual.Series); + Assert.Equal(tokens[1], actual.Volumes); + Assert.Equal(tokens[2], actual.Chapters); } [Theory] [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")] [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")] - [InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "manga")] + [InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] [InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] [InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")] [InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")] @@ -73,8 +74,8 @@ public class DefaultParserTests fs.AddDirectory(rootDirectory); fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); - var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); + var parser = new DefaultParser(ds); + var actual = parser.Parse(inputFile, rootDirectory); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -89,8 +90,8 @@ public class DefaultParserTests fs.AddDirectory(rootDirectory); fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); - var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); + var parser = new DefaultParser(ds); + var actual = parser.Parse(inputFile, rootDirectory); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -100,6 +101,13 @@ public class DefaultParserTests #region Parse + [Fact] + public void Parse_MangaLibrary_JustCover_ShouldReturnNull() + { + const string rootPath = @"E:/Manga/"; + var actual = _defaultParser.Parse(@"E:/Manga/Accel World/cover.png", rootPath); + Assert.Null(actual); + } [Fact] public void Parse_ParseInfo_Manga() @@ -119,20 +127,19 @@ public class DefaultParserTests expected.Add(filepath, new ParserInfo { Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Vol 1.cbz", Format = MangaFormat.Archive, + Chapters = "0", Filename = "Vol 1.cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Beelzebub/Beelzebub_01_[Noodles].zip"; + filepath = @"E:\Manga\Beelzebub\Beelzebub_01_[Noodles].zip"; expected.Add(filepath, new ParserInfo { - Series = "Beelzebub", Volumes = Parser.LooseLeafVolume, + Series = "Beelzebub", Volumes = "0", Chapters = "1", Filename = "Beelzebub_01_[Noodles].zip", Format = MangaFormat.Archive, FullFilePath = filepath }); - // Note: Lots of duplicates here. I think I can move them to the ParserTests itself - filepath = @"E:/Manga/Ichinensei ni Nacchattara/Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip"; + filepath = @"E:\Manga\Ichinensei ni Nacchattara\Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip"; expected.Add(filepath, new ParserInfo { Series = "Ichinensei ni Nacchattara", Volumes = "1", @@ -140,71 +147,71 @@ public class DefaultParserTests FullFilePath = filepath }); - filepath = @"E:/Manga/Tenjo Tenge (Color)/Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz"; + filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz"; expected.Add(filepath, new ParserInfo { Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive, + Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)/Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz"; + filepath = @"E:\Manga\Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)\Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz"; expected.Add(filepath, new ParserInfo { Series = "Akame ga KILL! ZERO", Volumes = "1", Edition = "", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive, + Chapters = "0", Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Dorohedoro/Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz"; + filepath = @"E:\Manga\Dorohedoro\Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz"; expected.Add(filepath, new ParserInfo { Series = "Dorohedoro", Volumes = "1", Edition = "", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive, + Chapters = "0", Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/APOSIMZ/APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz"; + filepath = @"E:\Manga\APOSIMZ\APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz"; expected.Add(filepath, new ParserInfo { - Series = "APOSIMZ", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "APOSIMZ", Volumes = "0", Edition = "", Chapters = "40", Filename = "APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Corpse Party Musume/Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz"; + filepath = @"E:\Manga\Corpse Party Musume\Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz"; expected.Add(filepath, new ParserInfo { - Series = "Kedouin Makoto - Corpse Party Musume", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Kedouin Makoto - Corpse Party Musume", Volumes = "0", Edition = "", Chapters = "9", Filename = "Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Goblin Slayer/Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz"; + filepath = @"E:\Manga\Goblin Slayer\Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz"; expected.Add(filepath, new ParserInfo { - Series = "Goblin Slayer - Brand New Day", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Goblin Slayer - Brand New Day", Volumes = "0", Edition = "", Chapters = "6.5", Filename = "Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive, FullFilePath = filepath }); - filepath = @"E:/Manga/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr"; + filepath = @"E:\Manga\Summer Time Rendering\Specials\Record 014 (between chapter 083 and ch084) SP11.cbr"; expected.Add(filepath, new ParserInfo { - Series = "Summer Time Rendering", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, Edition = "", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive, + Series = "Summer Time Rendering", Volumes = "0", Edition = "", + Chapters = "0", Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive, FullFilePath = filepath, IsSpecial = true }); - filepath = @"E:/Manga/Seraph of the End/Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz"; + filepath = @"E:\Manga\Seraph of the End\Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz"; expected.Add(filepath, new ParserInfo { - Series = "Seraph of the End - Vampire Reign", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Seraph of the End - Vampire Reign", Volumes = "0", Edition = "", Chapters = "93", Filename = "Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive, FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Manga/Kono Subarashii Sekai ni Bakuen wo!/Vol. 00 Ch. 000.cbz"; + filepath = @"E:\Manga\Kono Subarashii Sekai ni Bakuen wo!\Vol. 00 Ch. 000.cbz"; expected.Add(filepath, new ParserInfo { Series = "Kono Subarashii Sekai ni Bakuen wo!", Volumes = "0", Edition = "", @@ -212,7 +219,7 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Manga/Toukyou Akazukin/Vol. 01 Ch. 001.cbz"; + filepath = @"E:\Manga\Toukyou Akazukin\Vol. 01 Ch. 001.cbz"; expected.Add(filepath, new ParserInfo { Series = "Toukyou Akazukin", Volumes = "1", Edition = "", @@ -221,37 +228,62 @@ public class DefaultParserTests }); // If an image is cover exclusively, ignore it - filepath = @"E:/Manga/Seraph of the End/cover.png"; + filepath = @"E:\Manga\Seraph of the End\cover.png"; expected.Add(filepath, null); - filepath = @"E:/Manga/The Beginning After the End/Chapter 001.cbz"; + filepath = @"E:\Manga\The Beginning After the End\Chapter 001.cbz"; expected.Add(filepath, new ParserInfo { - Series = "The Beginning After the End", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "The Beginning After the End", Volumes = "0", Edition = "", Chapters = "1", Filename = "Chapter 001.cbz", Format = MangaFormat.Archive, FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Manga/Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz"; + // Note: Fallback to folder will parse Monster #8 and get Monster + filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg"; expected.Add(filepath, new ParserInfo { - Series = "Air Gear", Volumes = "1", Edition = "Omnibus", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive, + Series = "Monster", Volumes = "0", Edition = "", + Chapters = "1", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Manga/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub"; + filepath = @"E:\Manga\Air Gear\Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz"; + expected.Add(filepath, new ParserInfo + { + Series = "Air Gear", Volumes = "1", Edition = "Omnibus", + Chapters = "0", Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive, + FullFilePath = filepath, IsSpecial = false + }); + + filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch186\Vol. 19 p106.gif"; + expected.Add(filepath, new ParserInfo + { + Series = "Just Images the second", Volumes = "19", Edition = "", + Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, + FullFilePath = filepath, IsSpecial = false + }); + + filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch186\Vol. 19 p106.gif"; + expected.Add(filepath, new ParserInfo + { + Series = "Just Images the second", Volumes = "19", Edition = "", + Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, + FullFilePath = filepath, IsSpecial = false + }); + + filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub"; expected.Add(filepath, new ParserInfo { Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "", - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub, + Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub, FullFilePath = filepath, IsSpecial = false }); foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, true, null); + var actual = _defaultParser.Parse(file, rootPath); if (expectedInfo == null) { Assert.Null(actual); @@ -276,90 +308,6 @@ public class DefaultParserTests } } - //[Fact] - public void Parse_ParseInfo_Manga_ImageOnly() - { - // Images don't have root path as E:/Manga, but rather as the path of the folder - - // Note: Fallback to folder will parse Monster #8 and get Monster - var filepath = @"E:/Manga/Monster #8/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg"; - var expectedInfo2 = new ParserInfo - { - Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", - Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - - filepath = @"E:/Manga/Extra layer for no reason/Just Images the second/Vol19/ch. 186/Vol. 19 p106.gif"; - expectedInfo2 = new ParserInfo - { - Series = "Just Images the second", Volumes = "19", Edition = "", - Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - - filepath = @"E:/Manga/Extra layer for no reason/Just Images the second/Blank Folder/Vol19/ch. 186/Vol. 19 p106.gif"; - expectedInfo2 = new ParserInfo - { - Series = "Just Images the second", Volumes = "19", Edition = "", - Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - } - [Fact] public void Parse_ParseInfo_Manga_WithSpecialsFolder() { @@ -372,7 +320,7 @@ public class DefaultParserTests filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var parser = new BasicParser(ds, new ImageParser(ds)); + var parser = new DefaultParser(ds); var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz"; // There is a bad parse for series like "Foo 50", so we have parsed chapter as 50 @@ -383,7 +331,7 @@ public class DefaultParserTests FullFilePath = filepath }; - var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); + var actual = parser.Parse(filepath, rootPath); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); @@ -407,12 +355,12 @@ public class DefaultParserTests filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz"; expected = new ParserInfo { - Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, IsSpecial = true, - Chapters = Parser.DefaultChapter, Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, + Series = "Foo 50", Volumes = "0", IsSpecial = true, + Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, FullFilePath = filepath }; - actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); + actual = parser.Parse(filepath, rootPath); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expected.Format, actual.Format); @@ -437,26 +385,26 @@ public class DefaultParserTests [Fact] public void Parse_ParseInfo_Comic() { - const string rootPath = "E:/Comics/"; + const string rootPath = @"E:/Comics/"; var expected = new Dictionary(); var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr"; expected.Add(filepath, new ParserInfo { - Series = "Teen Titans", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive, + Series = "Teen Titans", Volumes = "0", + Chapters = "0", Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive, FullFilePath = filepath }); // Fallback test with bad naming - filepath = @"E:/Comics/Comics/Babe/Babe Vol.1 #1-4/Babe 01.cbr"; + filepath = @"E:\Comics\Comics\Babe\Babe Vol.1 #1-4\Babe 01.cbr"; expected.Add(filepath, new ParserInfo { - Series = "Babe", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Babe", Volumes = "0", Edition = "", Chapters = "1", Filename = "Babe 01.cbr", Format = MangaFormat.Archive, FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Comics/Comics/Publisher/Batman the Detective (2021)/Batman the Detective - v6 - 11 - (2021).cbr"; + filepath = @"E:\Comics\Comics\Publisher\Batman the Detective (2021)\Batman the Detective - v6 - 11 - (2021).cbr"; expected.Add(filepath, new ParserInfo { Series = "Batman the Detective", Volumes = "6", Edition = "", @@ -464,10 +412,10 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }); - filepath = @"E:/Comics/Comics/Batman - The Man Who Laughs #1 (2005)/Batman - The Man Who Laughs #1 (2005).cbr"; + filepath = @"E:\Comics\Comics\Batman - The Man Who Laughs #1 (2005)\Batman - The Man Who Laughs #1 (2005).cbr"; expected.Add(filepath, new ParserInfo { - Series = "Batman - The Man Who Laughs", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Batman - The Man Who Laughs", Volumes = "0", Edition = "", Chapters = "1", Filename = "Batman - The Man Who Laughs #1 (2005).cbr", Format = MangaFormat.Archive, FullFilePath = filepath, IsSpecial = false }); @@ -475,7 +423,7 @@ public class DefaultParserTests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, true, null); + var actual = _defaultParser.Parse(file, rootPath, LibraryType.Comic); if (expectedInfo == null) { Assert.Null(actual); diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parser/MangaParserTests.cs similarity index 84% rename from API.Tests/Parsing/MangaParsingTests.cs rename to API.Tests/Parser/MangaParserTests.cs index 53f2bc4c9..f89a411e6 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -1,10 +1,18 @@ using API.Entities.Enums; using Xunit; +using Xunit.Abstractions; -namespace API.Tests.Parsing; +namespace API.Tests.Parser; -public class MangaParsingTests +public class MangaParserTests { + private readonly ITestOutputHelper _testOutputHelper; + + public MangaParserTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Theory] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")] @@ -17,7 +25,7 @@ public class MangaParsingTests [InlineData("v001", "1")] [InlineData("Vol 1", "1")] [InlineData("vol_356-1", "356")] // Mangapy syntax - [InlineData("No Volume", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("No Volume", "0")] [InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")] [InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")] @@ -32,18 +40,18 @@ public class MangaParsingTests [InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")] [InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] - [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")] - [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "0")] [InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")] [InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")] [InlineData("Kodomo no Jikan vol. 1.cbz", "1")] [InlineData("Kodomo no Jikan vol. 10.cbz", "10")] - [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", "0")] [InlineData("Vagabond_v03", "3")] [InlineData("Mujaki No Rakune Volume 10.cbz", "10")] - [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "0")] [InlineData("Volume 12 - Janken Boy is Coming!.cbz", "12")] [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")] [InlineData("Gantz.V26.cbz", "26")] @@ -52,7 +60,7 @@ public class MangaParsingTests [InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")] [InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")] [InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")] - [InlineData("Noblesse - Episode 406 (52 Pages).7z", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Noblesse - Episode 406 (52 Pages).7z", "0")] [InlineData("X-Men v1 #201 (September 2007).cbz", "1")] [InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")] [InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")] @@ -64,21 +72,20 @@ public class MangaParsingTests [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")] [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")] [InlineData("Kebab Том 1 Глава 3", "1")] - [InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] + [InlineData("Манга Глава 2", "0")] [InlineData("Манга Тома 1-4", "1-4")] [InlineData("Манга Том 1-4", "1-4")] [InlineData("조선왕조실톡 106화", "106")] + [InlineData("죽음 13회", "13")] [InlineData("동의보감 13장", "13")] [InlineData("몰?루 아카이브 7.5권", "7.5")] [InlineData("63권#200", "63")] [InlineData("시즌34삽화2", "34")] [InlineData("Accel World Chapter 001 Volume 002", "2")] [InlineData("Accel World Volume 2", "2")] - [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] - [InlineData("Zom 100 - Bucket List of the Dead v01", "1")] public void ParseVolumeTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); } [Theory] @@ -131,6 +138,7 @@ public class MangaParsingTests [InlineData("Vagabond_v03", "Vagabond")] [InlineData("[AN] Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1", "Mahoutsukai to Deshi no Futekisetsu na Kankei")] [InlineData("Beelzebub_Side_Story_02_RHS.zip", "Beelzebub Side Story")] + [InlineData("[BAA]_Darker_than_Black_Omake-1.zip", "Darker than Black")] [InlineData("Baketeriya ch01-05.zip", "Baketeriya")] [InlineData("[PROzess]Kimi_ha_midara_na_Boku_no_Joou_-_Ch01", "Kimi ha midara na Boku no Joou")] [InlineData("[SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar", "NEEDLESS")] @@ -196,31 +204,21 @@ public class MangaParsingTests [InlineData("죠시라쿠! 2년 후 1권", "죠시라쿠! 2년 후")] [InlineData("test 2 years 1권", "test 2 years")] [InlineData("test 2 years 1화", "test 2 years")] - [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")] - [InlineData("Cynthia The Mission - c000 - c006 (v06)", "Cynthia The Mission")] - [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1", "เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท")] - [InlineData("Max Level Returner เล่มที่ 5", "Max Level Returner")] - [InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")] - [InlineData("不安の種\uff0b - 01", "不安の種\uff0b")] - [InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")] - [InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")] - [InlineData("Monster #8 Ch. 001", "Monster #8")] - [InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")] public void ParseSeriesTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); } [Theory] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")] - [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] - [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "0")] + [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "0")] [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")] - [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")] [InlineData("c001", "1")] - [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "0")] [InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")] [InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")] [InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")] @@ -243,7 +241,7 @@ public class MangaParsingTests [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")] [InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")] - [InlineData("Vol 1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Vol 1", "0")] [InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")] @@ -255,10 +253,10 @@ public class MangaParsingTests [InlineData("Fullmetal Alchemist chapters 101-108.cbz", "101-108")] [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "2")] [InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "71-79")] - [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", "0")] [InlineData("Beelzebub_153b_RHS.zip", "153.5")] [InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")] - [InlineData("Transferred to another world magical swordsman v1.1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Transferred to another world magical swordsman v1.1", "0")] [InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")] [InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")] [InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")] @@ -277,31 +275,24 @@ public class MangaParsingTests [InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")] [InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")] [InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")] - [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")] [InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")] - [InlineData("Samurai Jack Vol. 01 - The threads of Time", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")] [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")] [InlineData("자유록 13회#2", "13")] [InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")] [InlineData("[ハレム]ナナとカオル ~高校生のSMごっこ~ 第10話", "10")] - [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] + [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "0")] [InlineData("Kebab Том 1 Глава 3", "3")] [InlineData("Манга Глава 2", "2")] [InlineData("Манга 2 Глава", "2")] [InlineData("Манга Том 1 2 Глава", "2")] [InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Bleach 001-003", "1-3")] - [InlineData("Accel World Volume 2", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] - [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] - [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] - [InlineData("Adabana c00-02", "0-2")] - [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")] - [InlineData("Max Level Returner ตอนที่ 5", "5")] - [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] - [InlineData("Monster #8 Ch. 001", "1")] + [InlineData("Accel World Volume 2", "0")] public void ParseChaptersTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); } @@ -321,25 +312,25 @@ public class MangaParsingTests Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseEdition(input)); } [Theory] - [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)] - [InlineData("Beelzebub_Omake_June_2012_RHS", false)] + [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)] + [InlineData("Beelzebub_Omake_June_2012_RHS", true)] [InlineData("Beelzebub_Side_Story_02_RHS.zip", false)] - [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", false)] - [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", false)] - [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", false)] - [InlineData("Ani-Hina Art Collection.cbz", false)] - [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", false)] - [InlineData("A Town Where You Live - Bonus Chapter.zip", false)] + [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", true)] + [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)] + [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)] + [InlineData("Ani-Hina Art Collection.cbz", true)] + [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", true)] + [InlineData("A Town Where You Live - Bonus Chapter.zip", true)] [InlineData("Yuki Merry - 4-Komga Anthology", false)] - [InlineData("Beastars - SP01", true)] - [InlineData("Beastars SP01", true)] + [InlineData("Beastars - SP01", false)] + [InlineData("Beastars SP01", false)] [InlineData("The League of Extraordinary Gentlemen", false)] [InlineData("The League of Extra-ordinary Gentlemen", false)] [InlineData("Dr. Ramune - Mysterious Disease Specialist v01 (2020) (Digital) (danke-Empire)", false)] [InlineData("Hajime no Ippo - Artbook", false)] public void IsMangaSpecialTest(string input, bool expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsSpecial(input, LibraryType.Manga)); + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsMangaSpecial(input)); } [Theory] diff --git a/API.Tests/Parsing/ParserInfoTests.cs b/API.Tests/Parser/ParserInfoTests.cs similarity index 90% rename from API.Tests/Parsing/ParserInfoTests.cs rename to API.Tests/Parser/ParserInfoTests.cs index cbb8ae99a..e7c48317b 100644 --- a/API.Tests/Parsing/ParserInfoTests.cs +++ b/API.Tests/Parser/ParserInfoTests.cs @@ -2,7 +2,7 @@ using API.Services.Tasks.Scanner.Parser; using Xunit; -namespace API.Tests.Parsing; +namespace API.Tests.Parser; public class ParserInfoTests { @@ -11,14 +11,14 @@ public class ParserInfoTests { var p1 = new ParserInfo() { - Chapters = Parser.DefaultChapter, + Chapters = "0", Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", IsSpecial = false, Series = "darker than black", Title = "darker than black", - Volumes = Parser.LooseLeafVolume + Volumes = "0" }; var p2 = new ParserInfo() @@ -30,7 +30,7 @@ public class ParserInfoTests IsSpecial = false, Series = "darker than black", Title = "Darker Than Black", - Volumes = Parser.LooseLeafVolume + Volumes = "0" }; var expected = new ParserInfo() @@ -42,7 +42,7 @@ public class ParserInfoTests IsSpecial = false, Series = "darker than black", Title = "darker than black", - Volumes = Parser.LooseLeafVolume + Volumes = "0" }; p1.Merge(p2); @@ -62,12 +62,12 @@ public class ParserInfoTests IsSpecial = true, Series = "darker than black", Title = "darker than black", - Volumes = Parser.LooseLeafVolume + Volumes = "0" }; var p2 = new ParserInfo() { - Chapters = Parser.DefaultChapter, + Chapters = "0", Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", diff --git a/API.Tests/Parsing/ParsingTests.cs b/API.Tests/Parser/ParserTest.cs similarity index 89% rename from API.Tests/Parsing/ParsingTests.cs rename to API.Tests/Parser/ParserTest.cs index 7d5da4f9c..5bdd3eb6e 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -3,32 +3,18 @@ using System.Linq; using Xunit; using static API.Services.Tasks.Scanner.Parser.Parser; -namespace API.Tests.Parsing; +namespace API.Tests.Parser; -public class ParsingTests +public class ParserTests { [Fact] public void ShouldWork() { - var s = 6.5f.ToString(CultureInfo.InvariantCulture); + var s = 6.5f + ""; var a = float.Parse(s, CultureInfo.InvariantCulture); Assert.Equal(6.5f, a); - - s = 6.5f + ""; - a = float.Parse(s, CultureInfo.CurrentCulture); - Assert.Equal(6.5f, a); } - // [Theory] - // [InlineData("de-DE")] - // [InlineData("en-US")] - // public void ShouldParse(string culture) - // { - // var s = 6.5f + ""; - // var a = float.Parse(s, CultureInfo.CreateSpecificCulture(culture)); - // Assert.Equal(6.5f, a); - // } - [Theory] [InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")] [InlineData("Shmo, Joe", "Shmo, Joe")] @@ -43,7 +29,6 @@ public class ParsingTests [InlineData("DEAD Tube Prologue", "DEAD Tube Prologue")] [InlineData("DEAD Tube Prologue SP01", "DEAD Tube Prologue")] [InlineData("DEAD_Tube_Prologue SP01", "DEAD Tube Prologue")] - [InlineData("SP01 1. DEAD Tube Prologue", "1. DEAD Tube Prologue")] public void CleanSpecialTitleTest(string input, string expected) { Assert.Equal(expected, CleanSpecialTitle(input)); @@ -60,18 +45,6 @@ public class ParsingTests Assert.Equal(expected, HasSpecialMarker(input)); } - [Theory] - [InlineData("Beastars - SP01", 1)] - [InlineData("Beastars SP01", 1)] - [InlineData("Beastars Special 01", 0)] - [InlineData("Beastars Extra 01", 0)] - [InlineData("Batman Beyond - Return of the Joker (2001) SP01", 1)] - [InlineData("Batman Beyond - Return of the Joker (2001)", 0)] - public void ParseSpecialIndexTest(string input, int expected) - { - Assert.Equal(expected, ParseSpecialIndex(input)); - } - [Theory] [InlineData("0001", "1")] [InlineData("1", "1")] @@ -98,8 +71,7 @@ public class ParsingTests [InlineData("-The Title", false, "The Title")] [InlineData("- The Title", false, "The Title")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")] - [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", - true, "Batman - Detective Comics - Rebirth Deluxe Edition Book 04")] + [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")] [InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")] [InlineData("Witchblade 089 (2005) (Bittertek-DCP) (Top Cow (Image Comics))", true, "Witchblade 089")] [InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")] @@ -183,7 +155,6 @@ public class ParsingTests [InlineData("3.5", 3.5)] [InlineData("3.5-4.0", 3.5)] [InlineData("asdfasdf", 0.0)] - [InlineData("-10", -10.0)] public void MinimumNumberFromRangeTest(string input, float expected) { Assert.Equal(expected, MinNumberFromRange(input)); @@ -200,7 +171,6 @@ public class ParsingTests [InlineData("3.5", 3.5)] [InlineData("3.5-4.0", 4.0)] [InlineData("asdfasdf", 0.0)] - [InlineData("-10", -10.0)] public void MaximumNumberFromRangeTest(string input, float expected) { Assert.Equal(expected, MaxNumberFromRange(input)); @@ -216,7 +186,6 @@ public class ParsingTests [InlineData("카비타", "카비타")] [InlineData("06", "06")] [InlineData("", "")] - [InlineData("不安の種+", "不安の種+")] public void NormalizeTest(string input, string expected) { Assert.Equal(expected, Normalize(input)); @@ -251,7 +220,6 @@ public class ParsingTests [InlineData("ch1/backcover.png", false)] [InlineData("backcover.png", false)] [InlineData("back_cover.png", false)] - [InlineData("LD Blacklands #1 35 (back cover).png", false)] public void IsCoverImageTest(string inputPath, bool expected) { Assert.Equal(expected, IsCoverImage(inputPath)); @@ -267,7 +235,6 @@ public class ParsingTests [InlineData("@recycle/Love Hina/", true)] [InlineData("E:/Test/__MACOSX/Love Hina/", true)] [InlineData("E:/Test/.caltrash/Love Hina/", true)] - [InlineData("E:/Test/.yacreaderlibrary/Love Hina/", true)] public void HasBlacklistedFolderInPathTest(string inputPath, bool expected) { Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath)); diff --git a/API.Tests/Parsers/BasicParserTests.cs b/API.Tests/Parsers/BasicParserTests.cs deleted file mode 100644 index 32673e0e6..000000000 --- a/API.Tests/Parsers/BasicParserTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Parsers; - -public class BasicParserTests : AbstractFsTest -{ - private readonly BasicParser _parser; - private readonly ILogger _dsLogger = Substitute.For>(); - private readonly string _rootDirectory; - - public BasicParserTests() - { - var fileSystem = CreateFileSystem(); - _rootDirectory = Path.Join(DataDirectory, "Books/"); - fileSystem.AddDirectory(_rootDirectory); - fileSystem.AddFile($"{_rootDirectory}Harry Potter/Harry Potter - Vol 1.epub", new MockFileData("")); - - fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Volume 1.cbz", new MockFileData("")); - fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Volume 1 Chapter 2.cbz", new MockFileData("")); - fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Chapter 3.cbz", new MockFileData("")); - fileSystem.AddFile("$\"{RootDirectory}Accel World/Accel World Gaiden SP01.cbz", new MockFileData("")); - - - fileSystem.AddFile($"{_rootDirectory}Accel World/cover.png", new MockFileData("")); - - fileSystem.AddFile($"{_rootDirectory}Batman/Batman #1.cbz", new MockFileData("")); - - var ds = new DirectoryService(_dsLogger, fileSystem); - _parser = new BasicParser(ds, new ImageParser(ds)); - } - - #region Parse_Manga - - /// - /// Tests that when there is a loose-leaf cover in the manga library, that it is ignored - /// - [Fact] - public void Parse_MangaLibrary_JustCover_ShouldReturnNull() - { - var actual = _parser.Parse($"{_rootDirectory}Accel World/cover.png", $"{_rootDirectory}Accel World/", - _rootDirectory, LibraryType.Manga); - Assert.Null(actual); - } - - /// - /// Tests that when there is a loose-leaf cover in the manga library, that it is ignored - /// - [Fact] - public void Parse_MangaLibrary_OtherImage_ShouldReturnNull() - { - var actual = _parser.Parse($"{_rootDirectory}Accel World/page 01.png", $"{_rootDirectory}Accel World/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - } - - /// - /// Tests that when there is a volume and chapter in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_VolumeAndChapterInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz", $"{_rootDirectory}Mujaki no Rakuen/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Mujaki no Rakuen", actual.Series); - Assert.Equal("12", actual.Volumes); - Assert.Equal("76", actual.Chapters); - Assert.False(actual.IsSpecial); - } - - /// - /// Tests that when there is a volume in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_JustVolumeInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz", - $"{_rootDirectory}Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", actual.Series); - Assert.Equal("1", actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.False(actual.IsSpecial); - } - - /// - /// Tests that when there is a chapter only in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_JustChapterInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Beelzebub/Beelzebub_01_[Noodles].zip", - $"{_rootDirectory}Beelzebub/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Beelzebub", actual.Series); - Assert.Equal(Parser.LooseLeafVolume, actual.Volumes); - Assert.Equal("1", actual.Chapters); - Assert.False(actual.IsSpecial); - } - - /// - /// Tests that when there is a SP Marker in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_SpecialMarkerInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr", - $"{_rootDirectory}Summer Time Rendering/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Summer Time Rendering", actual.Series); - Assert.Equal(Parser.SpecialVolume, actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - - /// - /// Tests that when the filename parses as a special, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_SpecialInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Summer Time Rendering/Volume SP01.cbr", - $"{_rootDirectory}Summer Time Rendering/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Summer Time Rendering", actual.Series); - Assert.Equal("Volume", actual.Title); - Assert.Equal(Parser.SpecialVolume, actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - /// - /// Tests that when the filename parses as a special, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_SpecialInFilename2() - { - var actual = _parser.Parse("M:/Kimi wa Midara na Boku no Joou/Specials/[Renzokusei] Special 1 SP02.zip", - "M:/Kimi wa Midara na Boku no Joou/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Kimi wa Midara na Boku no Joou", actual.Series); - Assert.Equal("[Renzokusei] Special 1", actual.Title); - Assert.Equal(Parser.SpecialVolume, actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - /// - /// Tests that when the filename parses as a special, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_SpecialInFilename_StrangeNaming() - { - var actual = _parser.Parse($"{_rootDirectory}My Dress-Up Darling/SP01 1. Special Name.cbz", - _rootDirectory, - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("My Dress-Up Darling", actual.Series); - Assert.Equal("1. Special Name", actual.Title); - Assert.Equal(Parser.SpecialVolume, actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - /// - /// Tests that when there is an edition in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaLibrary_EditionInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", - $"{_rootDirectory}Air Gear/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Air Gear", actual.Series); - Assert.Equal("1", actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.False(actual.IsSpecial); - Assert.Equal("Omnibus", actual.Edition); - } - - #endregion - - #region Parse_Books - /// - /// Tests that when there is a volume in filename, it appropriately parses - /// - [Fact] - public void Parse_MangaBooks_JustVolumeInFilename() - { - var actual = _parser.Parse($"{_rootDirectory}Epubs/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", - $"{_rootDirectory}Epubs/", - _rootDirectory, LibraryType.Manga); - Assert.NotNull(actual); - - Assert.Equal("Harrison, Kim - The Good, The Bad, and the Undead - Hollows", actual.Series); - Assert.Equal("2.5", actual.Volumes); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - } - - #endregion - - #region IsApplicable - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Fails_WhenNonMatchingLibraryType() - { - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image)); - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.ComicVine)); - } - - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Success_WhenMatchingLibraryType() - { - Assert.True(_parser.IsApplicable("something.png", LibraryType.Manga)); - Assert.True(_parser.IsApplicable("something.png", LibraryType.Comic)); - Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book)); - Assert.True(_parser.IsApplicable("something.epub", LibraryType.LightNovel)); - } - - - #endregion -} diff --git a/API.Tests/Parsers/BookParserTests.cs b/API.Tests/Parsers/BookParserTests.cs deleted file mode 100644 index 90147ac6b..000000000 --- a/API.Tests/Parsers/BookParserTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Parsers; - -public class BookParserTests -{ - private readonly BookParser _parser; - private readonly ILogger _dsLogger = Substitute.For>(); - private const string RootDirectory = "C:/Books/"; - - public BookParserTests() - { - var fileSystem = new MockFileSystem(); - fileSystem.AddDirectory("C:/Books/"); - fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData("")); - fileSystem.AddFile("C:/Books/Adam Freeman - Pro ASP.NET Core 6.epub", new MockFileData("")); - fileSystem.AddFile("C:/Books/My Fav Book SP01.epub", new MockFileData("")); - var ds = new DirectoryService(_dsLogger, fileSystem); - _parser = new BookParser(ds, Substitute.For(), new BasicParser(ds, new ImageParser(ds))); - } - - #region Parse - - // TODO: I'm not sure how to actually test this as it relies on an epub parser to actually do anything - - /// - /// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter - /// - // [Fact] - // public void Parse_SeriesWithDirectoryName() - // { - // var actual = _parser.Parse("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", "C:/Books/Birds of Prey/", - // RootDirectory, LibraryType.Book, new ComicInfo() - // { - // Series = "Harry Potter", - // Volume = "1" - // }); - // - // Assert.NotNull(actual); - // Assert.Equal("Harry Potter", actual.Series); - // Assert.Equal("1", actual.Volumes); - // } - - #endregion - - #region IsApplicable - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Fails_WhenNonMatchingLibraryType() - { - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga)); - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Book)); - - } - - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Success_WhenMatchingLibraryType() - { - Assert.True(_parser.IsApplicable("something.epub", LibraryType.Image)); - } - #endregion -} diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/API.Tests/Parsers/ComicVineParserTests.cs deleted file mode 100644 index 2f4fd568e..000000000 --- a/API.Tests/Parsers/ComicVineParserTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Parsers; - -public class ComicVineParserTests -{ - private readonly ComicVineParser _parser; - private readonly ILogger _dsLogger = Substitute.For>(); - private const string RootDirectory = "C:/Comics/"; - - public ComicVineParserTests() - { - var fileSystem = new MockFileSystem(); - fileSystem.AddDirectory("C:/Comics/"); - fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)"); - fileSystem.AddFile("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", new MockFileData("")); - fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", new MockFileData("")); - fileSystem.AddFile("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", new MockFileData("")); - var ds = new DirectoryService(_dsLogger, fileSystem); - _parser = new ComicVineParser(ds); - } - - #region Parse - - /// - /// Tests that when Series and Volume are filled out, Kavita uses that for the Series Name - /// - [Fact] - public void Parse_SeriesWithComicInfo() - { - var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, true, new ComicInfo() - { - Series = "Birds of Prey", - Volume = "2002" - }); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey (2002)", actual.Series); - Assert.Equal("2002", actual.Volumes); - } - - /// - /// Tests that no ComicInfo, take the Directory Name if it matches "Series (2002)" or "Series (2)" - /// - [Fact] - public void Parse_SeriesWithDirectoryNameAsSeriesYear() - { - var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, true, null); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey (2002)", actual.Series); - Assert.Equal("2002", actual.Volumes); - Assert.Equal("1", actual.Chapters); - } - - /// - /// Tests that no ComicInfo, take a directory name up to root if it matches "Series (2002)" or "Series (2)" - /// - [Fact] - public void Parse_SeriesWithADirectoryNameAsSeriesYear() - { - var actual = _parser.Parse("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, true, null); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey (1999)", actual.Series); - Assert.Equal("1999", actual.Volumes); - Assert.Equal("1", actual.Chapters); - } - - /// - /// Tests that no ComicInfo and nothing matches Series (Volume), then just take the directory name as the Series - /// - [Fact] - public void Parse_FallbackToDirectoryNameOnly() - { - var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, true, null); - - Assert.NotNull(actual); - Assert.Equal("Blood Syndicate", actual.Series); - Assert.Equal(Parser.LooseLeafVolume, actual.Volumes); - Assert.Equal("1", actual.Chapters); - } - #endregion - - #region IsApplicable - /// - /// Tests that this Parser can only be used on ComicVine type - /// - [Fact] - public void IsApplicable_Fails_WhenNonMatchingLibraryType() - { - Assert.False(_parser.IsApplicable("", LibraryType.Comic)); - } - - /// - /// Tests that this Parser can only be used on ComicVine type - /// - [Fact] - public void IsApplicable_Success_WhenMatchingLibraryType() - { - Assert.True(_parser.IsApplicable("", LibraryType.ComicVine)); - } - #endregion -} diff --git a/API.Tests/Parsers/ImageParserTests.cs b/API.Tests/Parsers/ImageParserTests.cs deleted file mode 100644 index 63df1926e..000000000 --- a/API.Tests/Parsers/ImageParserTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Parsers; - -public class ImageParserTests -{ - private readonly ImageParser _parser; - private readonly ILogger _dsLogger = Substitute.For>(); - private const string RootDirectory = "C:/Comics/"; - - public ImageParserTests() - { - var fileSystem = new MockFileSystem(); - fileSystem.AddDirectory("C:/Comics/"); - fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)"); - fileSystem.AddFile("C:/Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData("")); - fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData("")); - var ds = new DirectoryService(_dsLogger, fileSystem); - _parser = new ImageParser(ds); - } - - #region Parse - - /// - /// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter - /// - [Fact] - public void Parse_SeriesWithDirectoryName() - { - var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/", - RootDirectory, LibraryType.Image, true, null); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey", actual.Series); - Assert.Equal("1", actual.Chapters); - } - - /// - /// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder - /// - [Fact] - public void Parse_SeriesWithNoNestedChapter() - { - var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, true, null); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey", actual.Series); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - } - - /// - /// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder and everything else as a - /// - [Fact] - public void Parse_SeriesWithLooseImages() - { - var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, true, null); - - Assert.NotNull(actual); - Assert.Equal("Birds of Prey", actual.Series); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - - #endregion - - #region IsApplicable - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Fails_WhenNonMatchingLibraryType() - { - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga)); - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image)); - Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image)); - } - - /// - /// Tests that this Parser can only be used on images and Image library type - /// - [Fact] - public void IsApplicable_Success_WhenMatchingLibraryType() - { - Assert.True(_parser.IsApplicable("something.png", LibraryType.Image)); - } - #endregion -} diff --git a/API.Tests/Parsers/PdfParserTests.cs b/API.Tests/Parsers/PdfParserTests.cs deleted file mode 100644 index 08bf9f25d..000000000 --- a/API.Tests/Parsers/PdfParserTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Parsers; - -public class PdfParserTests -{ - private readonly PdfParser _parser; - private readonly ILogger _dsLogger = Substitute.For>(); - private const string RootDirectory = "C:/Books/"; - - public PdfParserTests() - { - var fileSystem = new MockFileSystem(); - fileSystem.AddDirectory("C:/Books/"); - fileSystem.AddDirectory("C:/Books/Birds of Prey (2002)"); - fileSystem.AddFile("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", new MockFileData("")); - fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData("")); - var ds = new DirectoryService(_dsLogger, fileSystem); - _parser = new PdfParser(ds); - } - - #region Parse - - /// - /// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter - /// - [Fact] - public void Parse_Book_SeriesWithDirectoryName() - { - var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", - "C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/", - RootDirectory, LibraryType.Book, true, null); - - Assert.NotNull(actual); - Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series); - Assert.Equal(Parser.DefaultChapter, actual.Chapters); - Assert.True(actual.IsSpecial); - } - - #endregion - - #region IsApplicable - /// - /// Tests that this Parser can only be used on pdfs - /// - [Fact] - public void IsApplicable_Fails_WhenNonMatchingLibraryType() - { - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga)); - Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image)); - Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image)); - Assert.False(_parser.IsApplicable("something.png", LibraryType.Book)); - } - - /// - /// Tests that this Parser can only be used on pdfs - /// - [Fact] - public void IsApplicable_Success_WhenMatchingLibraryType() - { - Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book)); - Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Manga)); - } - #endregion -} diff --git a/API.Tests/Parsing/BookParsingTests.cs b/API.Tests/Parsing/BookParsingTests.cs deleted file mode 100644 index 9b02eff63..000000000 --- a/API.Tests/Parsing/BookParsingTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -using API.Entities.Enums; -using Xunit; - -namespace API.Tests.Parsing; - -public class BookParsingTests -{ - [Theory] - [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")] - [InlineData("BBC Focus 00 The Science of Happiness 2nd Edition (2018)", "BBC Focus 00 The Science of Happiness 2nd Edition")] - [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")] - public void ParseSeriesTest(string filename, string expected) - { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Book)); - } - - [Theory] - [InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")] - [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")] - public void ParseVolumeTest(string filename, string expected) - { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Book)); - } -} diff --git a/API.Tests/Parsing/ImageParsingTests.cs b/API.Tests/Parsing/ImageParsingTests.cs deleted file mode 100644 index 362b4b08c..000000000 --- a/API.Tests/Parsing/ImageParsingTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; -using Xunit.Abstractions; - -namespace API.Tests.Parsing; - -public class ImageParsingTests -{ - private readonly ITestOutputHelper _testOutputHelper; - private readonly ImageParser _parser; - - public ImageParsingTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - var directoryService = new DirectoryService(Substitute.For>(), new MockFileSystem()); - _parser = new ImageParser(directoryService); - } - - //[Fact] - public void Parse_ParseInfo_Manga_ImageOnly() - { - // Images don't have root path as E:\Manga, but rather as the path of the folder - - // Note: Fallback to folder will parse Monster #8 and get Monster - var filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg"; - var expectedInfo2 = new ParserInfo - { - Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", - Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - - filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch. 186\Vol. 19 p106.gif"; - expectedInfo2 = new ParserInfo - { - Series = "Just Images the second", Volumes = "19", Edition = "", - Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - - filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch. 186\Vol. 19 p106.gif"; - expectedInfo2 = new ParserInfo - { - Series = "Just Images the second", Volumes = "19", Edition = "", - Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image, - FullFilePath = filepath, IsSpecial = false - }; - - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); - Assert.NotNull(actual2); - _testOutputHelper.WriteLine($"Validating {filepath}"); - Assert.Equal(expectedInfo2.Format, actual2.Format); - _testOutputHelper.WriteLine("Format ✓"); - Assert.Equal(expectedInfo2.Series, actual2.Series); - _testOutputHelper.WriteLine("Series ✓"); - Assert.Equal(expectedInfo2.Chapters, actual2.Chapters); - _testOutputHelper.WriteLine("Chapters ✓"); - Assert.Equal(expectedInfo2.Volumes, actual2.Volumes); - _testOutputHelper.WriteLine("Volumes ✓"); - Assert.Equal(expectedInfo2.Edition, actual2.Edition); - _testOutputHelper.WriteLine("Edition ✓"); - Assert.Equal(expectedInfo2.Filename, actual2.Filename); - _testOutputHelper.WriteLine("Filename ✓"); - Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath); - _testOutputHelper.WriteLine("FullFilePath ✓"); - } -} diff --git a/API.Tests/Repository/CollectionTagRepositoryTests.cs b/API.Tests/Repository/CollectionTagRepositoryTests.cs index 5318260be..1859ab1fc 100644 --- a/API.Tests/Repository/CollectionTagRepositoryTests.cs +++ b/API.Tests/Repository/CollectionTagRepositoryTests.cs @@ -15,6 +15,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; +using Xunit; namespace API.Tests.Repository; @@ -113,65 +114,65 @@ public class CollectionTagRepositoryTests #endregion - // #region RemoveTagsWithoutSeries - // - // [Fact] - // public async Task RemoveTagsWithoutSeries_ShouldRemoveTags() - // { - // var library = new LibraryBuilder("Test", LibraryType.Manga).Build(); - // var series = new SeriesBuilder("Test 1").Build(); - // var commonTag = new AppUserCollectionBuilder("Tag 1").Build(); - // series.Metadata.CollectionTags.Add(commonTag); - // series.Metadata.CollectionTags.Add(new AppUserCollectionBuilder("Tag 2").Build()); - // - // var series2 = new SeriesBuilder("Test 1").Build(); - // series2.Metadata.CollectionTags.Add(commonTag); - // library.Series.Add(series); - // library.Series.Add(series2); - // _unitOfWork.LibraryRepository.Add(library); - // await _unitOfWork.CommitAsync(); - // - // Assert.Equal(2, series.Metadata.CollectionTags.Count); - // Assert.Single(series2.Metadata.CollectionTags); - // - // // Delete both series - // _unitOfWork.SeriesRepository.Remove(series); - // _unitOfWork.SeriesRepository.Remove(series2); - // - // await _unitOfWork.CommitAsync(); - // - // // Validate that both tags exist - // Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); - // - // await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); - // - // Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()); - // } - // - // [Fact] - // public async Task RemoveTagsWithoutSeries_ShouldNotRemoveTags() - // { - // var library = new LibraryBuilder("Test", LibraryType.Manga).Build(); - // var series = new SeriesBuilder("Test 1").Build(); - // var commonTag = new AppUserCollectionBuilder("Tag 1").Build(); - // series.Metadata.CollectionTags.Add(commonTag); - // series.Metadata.CollectionTags.Add(new AppUserCollectionBuilder("Tag 2").Build()); - // - // var series2 = new SeriesBuilder("Test 1").Build(); - // series2.Metadata.CollectionTags.Add(commonTag); - // library.Series.Add(series); - // library.Series.Add(series2); - // _unitOfWork.LibraryRepository.Add(library); - // await _unitOfWork.CommitAsync(); - // - // Assert.Equal(2, series.Metadata.CollectionTags.Count); - // Assert.Single(series2.Metadata.CollectionTags); - // - // await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); - // - // // Validate that both tags exist - // Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); - // } - // - // #endregion + #region RemoveTagsWithoutSeries + + [Fact] + public async Task RemoveTagsWithoutSeries_ShouldRemoveTags() + { + var library = new LibraryBuilder("Test", LibraryType.Manga).Build(); + var series = new SeriesBuilder("Test 1").Build(); + var commonTag = new CollectionTagBuilder("Tag 1").Build(); + series.Metadata.CollectionTags.Add(commonTag); + series.Metadata.CollectionTags.Add(new CollectionTagBuilder("Tag 2").Build()); + + var series2 = new SeriesBuilder("Test 1").Build(); + series2.Metadata.CollectionTags.Add(commonTag); + library.Series.Add(series); + library.Series.Add(series2); + _unitOfWork.LibraryRepository.Add(library); + await _unitOfWork.CommitAsync(); + + Assert.Equal(2, series.Metadata.CollectionTags.Count); + Assert.Single(series2.Metadata.CollectionTags); + + // Delete both series + _unitOfWork.SeriesRepository.Remove(series); + _unitOfWork.SeriesRepository.Remove(series2); + + await _unitOfWork.CommitAsync(); + + // Validate that both tags exist + Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); + + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + + Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()); + } + + [Fact] + public async Task RemoveTagsWithoutSeries_ShouldNotRemoveTags() + { + var library = new LibraryBuilder("Test", LibraryType.Manga).Build(); + var series = new SeriesBuilder("Test 1").Build(); + var commonTag = new CollectionTagBuilder("Tag 1").Build(); + series.Metadata.CollectionTags.Add(commonTag); + series.Metadata.CollectionTags.Add(new CollectionTagBuilder("Tag 2").Build()); + + var series2 = new SeriesBuilder("Test 1").Build(); + series2.Metadata.CollectionTags.Add(commonTag); + library.Series.Add(series); + library.Series.Add(series2); + _unitOfWork.LibraryRepository.Add(library); + await _unitOfWork.CommitAsync(); + + Assert.Equal(2, series.Metadata.CollectionTags.Count); + Assert.Single(series2.Metadata.CollectionTags); + + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + + // Validate that both tags exist + Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); + } + + #endregion } diff --git a/API.Tests/Repository/GenreRepositoryTests.cs b/API.Tests/Repository/GenreRepositoryTests.cs deleted file mode 100644 index d197a91ba..000000000 --- a/API.Tests/Repository/GenreRepositoryTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class GenreRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Genre.RemoveRange(Context.Genre); - Context.Library.RemoveRange(Context.Library); - await Context.SaveChangesAsync(); - } - - private TestGenreSet CreateTestGenres() - { - return new TestGenreSet - { - SharedSeriesChaptersGenre = new GenreBuilder("Shared Series Chapter Genre").Build(), - SharedSeriesGenre = new GenreBuilder("Shared Series Genre").Build(), - SharedChaptersGenre = new GenreBuilder("Shared Chapters Genre").Build(), - Lib0SeriesChaptersGenre = new GenreBuilder("Lib0 Series Chapter Genre").Build(), - Lib0SeriesGenre = new GenreBuilder("Lib0 Series Genre").Build(), - Lib0ChaptersGenre = new GenreBuilder("Lib0 Chapters Genre").Build(), - Lib1SeriesChaptersGenre = new GenreBuilder("Lib1 Series Chapter Genre").Build(), - Lib1SeriesGenre = new GenreBuilder("Lib1 Series Genre").Build(), - Lib1ChaptersGenre = new GenreBuilder("Lib1 Chapters Genre").Build(), - Lib1ChapterAgeGenre = new GenreBuilder("Lib1 Chapter Age Genre").Build() - }; - } - - private async Task SeedDbWithGenres(TestGenreSet genres) - { - await CreateTestUsers(); - await AddGenresToContext(genres); - await CreateLibrariesWithGenres(genres); - await AssignLibrariesToUsers(); - } - - private async Task CreateTestUsers() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.Users.Add(_fullAccess); - Context.Users.Add(_restrictedAccess); - Context.Users.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - } - - private async Task AddGenresToContext(TestGenreSet genres) - { - var allGenres = genres.GetAllGenres(); - Context.Genre.AddRange(allGenres); - await Context.SaveChangesAsync(); - } - - private async Task CreateLibrariesWithGenres(TestGenreSet genres) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0SeriesGenre]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre, genres.Lib1ChapterAgeGenre]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .Build()) - .Build()) - .Build(); - - Context.Library.Add(lib0); - Context.Library.Add(lib1); - await Context.SaveChangesAsync(); - } - - private async Task AssignLibrariesToUsers() - { - var lib0 = Context.Library.First(l => l.Name == "lib0"); - var lib1 = Context.Library.First(l => l.Name == "lib1"); - - _fullAccess.Libraries.Add(lib0); - _fullAccess.Libraries.Add(lib1); - _restrictedAccess.Libraries.Add(lib1); - _restrictedAgeAccess.Libraries.Add(lib1); - - await Context.SaveChangesAsync(); - } - - private static Predicate ContainsGenreCheck(Genre genre) - { - return g => g.Id == genre.Id; - } - - private static void AssertGenrePresent(IEnumerable genres, Genre expectedGenre) - { - Assert.Contains(genres, ContainsGenreCheck(expectedGenre)); - } - - private static void AssertGenreNotPresent(IEnumerable genres, Genre expectedGenre) - { - Assert.DoesNotContain(genres, ContainsGenreCheck(expectedGenre)); - } - - private static BrowseGenreDto GetGenreDto(IEnumerable genres, Genre genre) - { - return genres.First(dto => dto.Id == genre.Id); - } - - [Fact] - public async Task GetBrowseableGenre_FullAccess_ReturnsAllGenresWithCorrectCounts() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var fullAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_fullAccess.Id, new UserParams()); - - // Assert - Assert.Equal(genres.GetAllGenres().Count, fullAccessGenres.TotalCount); - - foreach (var genre in genres.GetAllGenres()) - { - AssertGenrePresent(fullAccessGenres, genre); - } - - // Verify counts - 1 lib0 series, 2 lib1 series = 3 total series - Assert.Equal(3, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(6, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(1, GetGenreDto(fullAccessGenres, genres.Lib0SeriesGenre).SeriesCount); - } - - [Fact] - public async Task GetBrowseableGenre_RestrictedAccess_ReturnsOnlyAccessibleGenres() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var restrictedAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 4 library 1 specific = 7 genres - Assert.Equal(7, restrictedAccessGenres.TotalCount); - - // Verify shared and Library 1 genres are present - AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesGenre); - AssertGenrePresent(restrictedAccessGenres, genres.SharedChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChapterAgeGenre); - - // Verify Library 0 specific genres are not present - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesChaptersGenre); - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesGenre); - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0ChaptersGenre); - - // Verify counts - 2 lib1 series - Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.Lib1SeriesGenre).SeriesCount); - Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); - Assert.Equal(1, GetGenreDto(restrictedAccessGenres, genres.Lib1ChapterAgeGenre).ChapterCount); - } - - [Fact] - public async Task GetBrowseableGenre_RestrictedAgeAccess_FiltersAgeRestrictedContent() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var restrictedAgeAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAgeAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 3 lib1 specific = 6 genres (age-restricted genre filtered out) - Assert.Equal(6, restrictedAgeAccessGenres.TotalCount); - - // Verify accessible genres are present - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre); - - // Verify age-restricted genre is filtered out - AssertGenreNotPresent(restrictedAgeAccessGenres, genres.Lib1ChapterAgeGenre); - - // Verify counts - 1 series lib1 (age-restricted series filtered out) - Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1SeriesGenre).SeriesCount); - - // These values represent a bug - chapters are not properly filtered when their series is age-restricted - // Should be 2, but currently returns 3 due to the filtering issue - Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); - } - - private class TestGenreSet - { - public Genre SharedSeriesChaptersGenre { get; set; } - public Genre SharedSeriesGenre { get; set; } - public Genre SharedChaptersGenre { get; set; } - public Genre Lib0SeriesChaptersGenre { get; set; } - public Genre Lib0SeriesGenre { get; set; } - public Genre Lib0ChaptersGenre { get; set; } - public Genre Lib1SeriesChaptersGenre { get; set; } - public Genre Lib1SeriesGenre { get; set; } - public Genre Lib1ChaptersGenre { get; set; } - public Genre Lib1ChapterAgeGenre { get; set; } - - public List GetAllGenres() - { - return - [ - SharedSeriesChaptersGenre, SharedSeriesGenre, SharedChaptersGenre, - Lib0SeriesChaptersGenre, Lib0SeriesGenre, Lib0ChaptersGenre, - Lib1SeriesChaptersGenre, Lib1SeriesGenre, Lib1ChaptersGenre, Lib1ChapterAgeGenre - ]; - } - } -} diff --git a/API.Tests/Repository/PersonRepositoryTests.cs b/API.Tests/Repository/PersonRepositoryTests.cs deleted file mode 100644 index a2b19cc0c..000000000 --- a/API.Tests/Repository/PersonRepositoryTests.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class PersonRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); - Context.AppUser.RemoveRange(Context.AppUser.ToList()); - await UnitOfWork.CommitAsync(); - } - - private async Task SeedDb() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.AppUser.Add(_fullAccess); - Context.AppUser.Add(_restrictedAccess); - Context.AppUser.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - - var people = CreateTestPeople(); - Context.Person.AddRange(people); - await Context.SaveChangesAsync(); - - var libraries = CreateTestLibraries(people); - Context.Library.AddRange(libraries); - await Context.SaveChangesAsync(); - - _fullAccess.Libraries.Add(libraries[0]); // lib0 - _fullAccess.Libraries.Add(libraries[1]); // lib1 - _restrictedAccess.Libraries.Add(libraries[1]); // lib1 only - _restrictedAgeAccess.Libraries.Add(libraries[1]); // lib1 only - - await Context.SaveChangesAsync(); - } - - private static List CreateTestPeople() - { - return new List - { - new PersonBuilder("Shared Series Chapter Person").Build(), - new PersonBuilder("Shared Series Person").Build(), - new PersonBuilder("Shared Chapters Person").Build(), - new PersonBuilder("Lib0 Series Chapter Person").Build(), - new PersonBuilder("Lib0 Series Person").Build(), - new PersonBuilder("Lib0 Chapters Person").Build(), - new PersonBuilder("Lib1 Series Chapter Person").Build(), - new PersonBuilder("Lib1 Series Person").Build(), - new PersonBuilder("Lib1 Chapters Person").Build(), - new PersonBuilder("Lib1 Chapter Age Person").Build() - }; - } - - private static List CreateTestLibraries(List people) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Lib0 Series Person"), PersonRole.Writer) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Colorist) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Editor) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Letterer) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Imprint) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Chapter Age Person"), PersonRole.CoverArtist) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Inker) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Team) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Translator) - .Build()) - .Build()) - .Build()) - .Build(); - - return new List { lib0, lib1 }; - } - - private static Person GetPersonByName(List people, string name) - { - return people.First(p => p.Name == name); - } - - private Person GetPersonByName(string name) - { - return Context.Person.First(p => p.Name == name); - } - - private static Predicate ContainsPersonCheck(Person person) - { - return p => p.Id == person.Id; - } - - [Fact] - public async Task GetBrowsePersonDtos() - { - await ResetDb(); - await SeedDb(); - - // Get people from database for assertions - var sharedSeriesChaptersPerson = GetPersonByName("Shared Series Chapter Person"); - var lib0SeriesPerson = GetPersonByName("Lib0 Series Person"); - var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); - var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); - var allPeople = Context.Person.ToList(); - - var fullAccessPeople = - await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_fullAccess.Id, new BrowsePersonFilterDto(), - new UserParams()); - Assert.Equal(allPeople.Count, fullAccessPeople.TotalCount); - - foreach (var person in allPeople) - Assert.Contains(fullAccessPeople, ContainsPersonCheck(person)); - - // 1 series in lib0, 2 series in lib1 - Assert.Equal(3, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); - // 3 series with each 2 chapters - Assert.Equal(6, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); - // 1 series in lib0 - Assert.Equal(1, fullAccessPeople.First(dto => dto.Id == lib0SeriesPerson.Id).SeriesCount); - // 2 series in lib1 - Assert.Equal(2, fullAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); - - var restrictedAccessPeople = - await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAccess.Id, new BrowsePersonFilterDto(), - new UserParams()); - - Assert.Equal(7, restrictedAccessPeople.TotalCount); - - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Chapter Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Chapters Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Chapter Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapters Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapter Age Person"))); - - // 2 series in lib1, no series in lib0 - Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); - // 2 series with each 2 chapters - Assert.Equal(4, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); - // 2 series in lib1 - Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); - - var restrictedAgeAccessPeople = await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAgeAccess.Id, - new BrowsePersonFilterDto(), new UserParams()); - - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Equal(6, restrictedAgeAccessPeople.TotalCount); - - // No access to the age restricted chapter - Assert.DoesNotContain(restrictedAgeAccessPeople, ContainsPersonCheck(lib1ChapterAgePerson)); - } - - [Fact] - public async Task GetRolesForPersonByName() - { - await ResetDb(); - await SeedDb(); - - var sharedSeriesPerson = GetPersonByName("Shared Series Person"); - var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); - var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); - - var sharedSeriesRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _fullAccess.Id); - var chapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _fullAccess.Id); - var ageChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _fullAccess.Id); - Assert.Equal(3, sharedSeriesRoles.Count()); - Assert.Equal(6, chapterRoles.Count()); - Assert.Single(ageChapterRoles); - - var restrictedRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAccess.Id); - var restrictedChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAccess.Id); - var restrictedAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAccess.Id); - Assert.Equal(2, restrictedRoles.Count()); - Assert.Equal(4, restrictedChapterRoles.Count()); - Assert.Single(restrictedAgePersonChapterRoles); - - var restrictedAgeRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); - var restrictedAgeChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAgeAccess.Id); - var restrictedAgeAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAgeAccess.Id); - Assert.Single(restrictedAgeRoles); - Assert.Equal(2, restrictedAgeChapterRoles.Count()); - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Empty(restrictedAgeAgePersonChapterRoles); - } - - [Fact] - public async Task GetPersonDtoByName() - { - await ResetDb(); - await SeedDb(); - - var allPeople = Context.Person.ToList(); - - foreach (var person in allPeople) - { - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName(person.Name, _fullAccess.Id)); - } - - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Shared Series Person", _restrictedAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAccess.Id)); - - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAgeAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAgeAccess.Id)); - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Chapter Age Person", _restrictedAgeAccess.Id)); - } - - [Fact] - public async Task GetSeriesKnownFor() - { - await ResetDb(); - await SeedDb(); - - var sharedSeriesPerson = GetPersonByName("Shared Series Person"); - var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); - - var series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _fullAccess.Id); - Assert.Equal(3, series.Count()); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAccess.Id); - Assert.Equal(2, series.Count()); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); - Assert.Single(series); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, _restrictedAgeAccess.Id); - Assert.Single(series); - } - - [Fact] - public async Task GetChaptersForPersonByRole() - { - await ResetDb(); - await SeedDb(); - - var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); - - // Lib0 - var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Colorist); - var restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Colorist); - var restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Colorist); - Assert.Single(chapters); - Assert.Empty(restrictedChapters); - Assert.Empty(restrictedAgeChapters); - - // Lib1 - age restricted series - chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Imprint); - restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Imprint); - restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Imprint); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Empty(restrictedAgeChapters); - - // Lib1 - not age restricted series - chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Team); - restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Team); - restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Team); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Single(restrictedAgeChapters); - } -} diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index 5705e1bc0..ec4b2a9f5 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; @@ -158,6 +159,4 @@ public class SeriesRepositoryTests } } - // TODO: GetSeriesDtoForLibraryIdV2Async Tests (On Deck) - } diff --git a/API.Tests/Repository/TagRepositoryTests.cs b/API.Tests/Repository/TagRepositoryTests.cs deleted file mode 100644 index 229082eb6..000000000 --- a/API.Tests/Repository/TagRepositoryTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class TagRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Tag.RemoveRange(Context.Tag); - Context.Library.RemoveRange(Context.Library); - await Context.SaveChangesAsync(); - } - - private TestTagSet CreateTestTags() - { - return new TestTagSet - { - SharedSeriesChaptersTag = new TagBuilder("Shared Series Chapter Tag").Build(), - SharedSeriesTag = new TagBuilder("Shared Series Tag").Build(), - SharedChaptersTag = new TagBuilder("Shared Chapters Tag").Build(), - Lib0SeriesChaptersTag = new TagBuilder("Lib0 Series Chapter Tag").Build(), - Lib0SeriesTag = new TagBuilder("Lib0 Series Tag").Build(), - Lib0ChaptersTag = new TagBuilder("Lib0 Chapters Tag").Build(), - Lib1SeriesChaptersTag = new TagBuilder("Lib1 Series Chapter Tag").Build(), - Lib1SeriesTag = new TagBuilder("Lib1 Series Tag").Build(), - Lib1ChaptersTag = new TagBuilder("Lib1 Chapters Tag").Build(), - Lib1ChapterAgeTag = new TagBuilder("Lib1 Chapter Age Tag").Build() - }; - } - - private async Task SeedDbWithTags(TestTagSet tags) - { - await CreateTestUsers(); - await AddTagsToContext(tags); - await CreateLibrariesWithTags(tags); - await AssignLibrariesToUsers(); - } - - private async Task CreateTestUsers() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.Users.Add(_fullAccess); - Context.Users.Add(_restrictedAccess); - Context.Users.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - } - - private async Task AddTagsToContext(TestTagSet tags) - { - var allTags = tags.GetAllTags(); - Context.Tag.AddRange(allTags); - await Context.SaveChangesAsync(); - } - - private async Task CreateLibrariesWithTags(TestTagSet tags) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadata - { - Tags = [tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib0SeriesChaptersTag, tags.Lib0SeriesTag] - }) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib0SeriesChaptersTag, tags.Lib0ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag, tags.Lib1ChapterAgeTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .Build(); - - Context.Library.Add(lib0); - Context.Library.Add(lib1); - await Context.SaveChangesAsync(); - } - - private async Task AssignLibrariesToUsers() - { - var lib0 = Context.Library.First(l => l.Name == "lib0"); - var lib1 = Context.Library.First(l => l.Name == "lib1"); - - _fullAccess.Libraries.Add(lib0); - _fullAccess.Libraries.Add(lib1); - _restrictedAccess.Libraries.Add(lib1); - _restrictedAgeAccess.Libraries.Add(lib1); - - await Context.SaveChangesAsync(); - } - - private static Predicate ContainsTagCheck(Tag tag) - { - return t => t.Id == tag.Id; - } - - private static void AssertTagPresent(IEnumerable tags, Tag expectedTag) - { - Assert.Contains(tags, ContainsTagCheck(expectedTag)); - } - - private static void AssertTagNotPresent(IEnumerable tags, Tag expectedTag) - { - Assert.DoesNotContain(tags, ContainsTagCheck(expectedTag)); - } - - private static BrowseTagDto GetTagDto(IEnumerable tags, Tag tag) - { - return tags.First(dto => dto.Id == tag.Id); - } - - [Fact] - public async Task GetBrowseableTag_FullAccess_ReturnsAllTagsWithCorrectCounts() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var fullAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_fullAccess.Id, new UserParams()); - - // Assert - Assert.Equal(tags.GetAllTags().Count, fullAccessTags.TotalCount); - - foreach (var tag in tags.GetAllTags()) - { - AssertTagPresent(fullAccessTags, tag); - } - - // Verify counts - 1 series lib0, 2 series lib1 = 3 total series - Assert.Equal(3, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(6, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(1, GetTagDto(fullAccessTags, tags.Lib0SeriesTag).SeriesCount); - } - - [Fact] - public async Task GetBrowseableTag_RestrictedAccess_ReturnsOnlyAccessibleTags() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var restrictedAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 4 library 1 specific = 7 tags - Assert.Equal(7, restrictedAccessTags.TotalCount); - - // Verify shared and Library 1 tags are present - AssertTagPresent(restrictedAccessTags, tags.SharedSeriesChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.SharedSeriesTag); - AssertTagPresent(restrictedAccessTags, tags.SharedChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1ChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1ChapterAgeTag); - - // Verify Library 0 specific tags are not present - AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesChaptersTag); - AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesTag); - AssertTagNotPresent(restrictedAccessTags, tags.Lib0ChaptersTag); - - // Verify counts - 2 series lib1 - Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.Lib1SeriesTag).SeriesCount); - Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.Lib1ChaptersTag).ChapterCount); - } - - [Fact] - public async Task GetBrowseableTag_RestrictedAgeAccess_FiltersAgeRestrictedContent() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var restrictedAgeAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAgeAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 3 lib1 specific = 6 tags (age-restricted tag filtered out) - Assert.Equal(6, restrictedAgeAccessTags.TotalCount); - - // Verify accessible tags are present - AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesTag); - AssertTagPresent(restrictedAgeAccessTags, tags.SharedChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1ChaptersTag); - - // Verify age-restricted tag is filtered out - AssertTagNotPresent(restrictedAgeAccessTags, tags.Lib1ChapterAgeTag); - - // Verify counts - 1 series lib1 (age-restricted series filtered out) - Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.Lib1SeriesTag).SeriesCount); - Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.Lib1ChaptersTag).ChapterCount); - } - - private class TestTagSet - { - public Tag SharedSeriesChaptersTag { get; set; } - public Tag SharedSeriesTag { get; set; } - public Tag SharedChaptersTag { get; set; } - public Tag Lib0SeriesChaptersTag { get; set; } - public Tag Lib0SeriesTag { get; set; } - public Tag Lib0ChaptersTag { get; set; } - public Tag Lib1SeriesChaptersTag { get; set; } - public Tag Lib1SeriesTag { get; set; } - public Tag Lib1ChaptersTag { get; set; } - public Tag Lib1ChapterAgeTag { get; set; } - - public List GetAllTags() - { - return - [ - SharedSeriesChaptersTag, SharedSeriesTag, SharedChaptersTag, - Lib0SeriesChaptersTag, Lib0SeriesTag, Lib0ChaptersTag, - Lib1SeriesChaptersTag, Lib1SeriesTag, Lib1ChaptersTag, Lib1ChapterAgeTag - ]; - } - } -} diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 8cf93df37..086d99863 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -7,6 +7,7 @@ using System.Linq; using API.Archive; using API.Entities.Enums; using API.Services; +using EasyCaching.Core; using Microsoft.Extensions.Logging; using NetVips; using NSubstitute; @@ -28,7 +29,7 @@ public class ArchiveServiceTests { _testOutputHelper = testOutputHelper; _archiveService = new ArchiveService(_logger, _directoryService, - new ImageService(Substitute.For>(), _directoryService), + new ImageService(Substitute.For>(), _directoryService, Substitute.For()), Substitute.For()); } @@ -166,7 +167,7 @@ public class ArchiveServiceTests public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile) { var ds = Substitute.For(_directoryServiceLogger, new FileSystem()); - var imageService = new ImageService(Substitute.For>(), ds); + var imageService = new ImageService(Substitute.For>(), ds, Substitute.For()); var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); @@ -197,7 +198,7 @@ public class ArchiveServiceTests [InlineData("sorting.zip", "sorting.expected.png")] public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile) { - var imageService = new ImageService(Substitute.For>(), _directoryService); + var imageService = new ImageService(Substitute.For>(), _directoryService, Substitute.For()); var archiveService = Substitute.For(_logger, new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService, Substitute.For()); diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index aac5724f7..c4ca95a11 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,8 +1,10 @@ -using System.Data.Common; +using System.Collections.Generic; +using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; +using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; @@ -19,7 +21,7 @@ using Xunit; namespace API.Tests.Services; -public class BackupServiceTests: AbstractFsTest +public class BackupServiceTests { private readonly ILogger _logger = Substitute.For>(); private readonly IUnitOfWork _unitOfWork; @@ -29,6 +31,13 @@ public class BackupServiceTests: AbstractFsTest private readonly DbConnection _connection; private readonly DataContext _context; + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string LogDirectory = "C:/kavita/config/logs/"; + private const string ConfigDirectory = "C:/kavita/config/"; + private const string BookmarkDirectory = "C:/kavita/config/bookmarks"; + private const string ThemesDirectory = "C:/kavita/config/theme"; public BackupServiceTests() { @@ -73,7 +82,7 @@ public class BackupServiceTests: AbstractFsTest _context.ServerSetting.Update(setting); _context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder(Root + "data/").Build()) + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) .Build()); return await _context.SaveChangesAsync() > 0; } @@ -85,6 +94,22 @@ public class BackupServiceTests: AbstractFsTest await _context.SaveChangesAsync(); } + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(LogDirectory); + fileSystem.AddDirectory(ThemesDirectory); + fileSystem.AddDirectory(BookmarkDirectory); + fileSystem.AddDirectory("C:/data/"); + + return fileSystem; + } + #endregion diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index 5848c74ba..e4647524e 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,8 +1,7 @@ using System.IO; using System.IO.Abstractions; -using API.Entities.Enums; using API.Services; -using API.Services.Tasks.Scanner.Parser; +using EasyCaching.Core; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -18,7 +17,7 @@ public class BookServiceTests { var directoryService = new DirectoryService(Substitute.For>(), new FileSystem()); _bookService = new BookService(_logger, directoryService, - new ImageService(Substitute.For>(), directoryService) + new ImageService(Substitute.For>(), directoryService, Substitute.For()) , Substitute.For()); } @@ -82,64 +81,4 @@ public class BookServiceTests Assert.Equal("Accel World", comicInfo.Series); } - [Fact] - public void ShouldHaveComicInfoForPdf() - { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); - var document = Path.Join(testDirectory, "test.pdf"); - var comicInfo = _bookService.GetComicInfo(document); - Assert.NotNull(comicInfo); - Assert.Equal("Variations Chromatiques de concert", comicInfo.Title); - Assert.Equal("Georges Bizet \\(1838-1875\\)", comicInfo.Writer); - } - - //[Fact] - public void ShouldUsePdfInfoDict() - { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); - var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); - var comicInfo = _bookService.GetComicInfo(document); - Assert.NotNull(comicInfo); - Assert.Equal("Rollo at Work", comicInfo.Title); - Assert.Equal("Jacob Abbott", comicInfo.Writer); - Assert.Equal(2008, comicInfo.Year); - } - - [Fact] - public void ShouldHandleIndirectPdfObjects() - { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); - var document = Path.Join(testDirectory, "indirect.pdf"); - var comicInfo = _bookService.GetComicInfo(document); - Assert.NotNull(comicInfo); - Assert.Equal(2018, comicInfo.Year); - Assert.Equal(8, comicInfo.Month); - } - - [Fact] - public void FailGracefullyWithEncryptedPdf() - { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); - var document = Path.Join(testDirectory, "encrypted.pdf"); - var comicInfo = _bookService.GetComicInfo(document); - Assert.Null(comicInfo); - } - - [Fact] - public void SeriesFallBackToMetadataTitle() - { - var ds = new DirectoryService(Substitute.For>(), new FileSystem()); - var pdfParser = new PdfParser(ds); - - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); - var filePath = Path.Join(testDirectory, "Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf"); - - var comicInfo = _bookService.GetComicInfo(filePath); - Assert.NotNull(comicInfo); - - var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, true, comicInfo); - Assert.NotNull(parserInfo); - Assert.Equal(parserInfo.Title, comicInfo.Title); - Assert.Equal(parserInfo.Series, comicInfo.Title); - } } diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 596fbbc4d..25c7bd5de 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -9,9 +9,12 @@ using API.Data.Repositories; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; +using API.SignalR; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -22,12 +25,17 @@ using Xunit; namespace API.Tests.Services; -public class BookmarkServiceTests: AbstractFsTest +public class BookmarkServiceTests { private readonly IUnitOfWork _unitOfWork; private readonly DbConnection _connection; private readonly DataContext _context; + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; + public BookmarkServiceTests() { @@ -80,7 +88,7 @@ Substitute.For()); _context.ServerSetting.Update(setting); _context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder(Root + "data/").Build()) + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) .Build()); return await _context.SaveChangesAsync() > 0; } @@ -94,6 +102,20 @@ Substitute.For()); await _context.SaveChangesAsync(); } + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(BookmarkDirectory); + fileSystem.AddDirectory("C:/data/"); + + return fileSystem; + } + #endregion #region BookmarkPage @@ -110,7 +132,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -158,8 +180,8 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") .Build()) .Build()) .Build(); @@ -224,7 +246,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -300,7 +322,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -353,7 +375,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -406,7 +428,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index caf1ae393..e1419e052 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,10 +1,12 @@ -using System.Data.Common; +using System.Collections.Generic; +using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; +using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; @@ -50,17 +52,17 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService throw new System.NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) + public ParserInfo Parse(string path, string rootPath, LibraryType type) { throw new System.NotImplementedException(); } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) + public ParserInfo ParseFile(string path, string rootPath, LibraryType type) { throw new System.NotImplementedException(); } } -public class CacheServiceTests: AbstractFsTest +public class CacheServiceTests { private readonly ILogger _logger = Substitute.For>(); private readonly IUnitOfWork _unitOfWork; @@ -69,6 +71,11 @@ public class CacheServiceTests: AbstractFsTest private readonly DbConnection _connection; private readonly DataContext _context; + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string DataDirectory = "C:/data/"; + public CacheServiceTests() { var contextOptions = new DbContextOptionsBuilder() @@ -111,7 +118,7 @@ public class CacheServiceTests: AbstractFsTest _context.ServerSetting.Update(setting); _context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder(Root + "data/").Build()) + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) .Build()); return await _context.SaveChangesAsync() > 0; } @@ -123,6 +130,19 @@ public class CacheServiceTests: AbstractFsTest await _context.SaveChangesAsync(); } + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(DataDirectory); + + return fileSystem; + } + #endregion #region Ensure @@ -136,9 +156,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), - Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); await ResetDB(); var s = new SeriesBuilder("Test").Build(); @@ -213,8 +231,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -235,15 +252,14 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); var c = new ChapterBuilder("1") .WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build()) .WithFile(new MangaFileBuilder($"{DataDirectory}2.epub", MangaFormat.Epub).Build()) .Build(); cs.GetCachedFile(c); - Assert.Equal($"{DataDirectory}1.epub", cs.GetCachedFile(c)); + Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c)); } #endregion @@ -276,8 +292,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -320,8 +335,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -361,8 +375,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -406,8 +419,7 @@ public class CacheServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), - Substitute.For(), Substitute.For(), ds, Substitute.For>()), - Substitute.For()); + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index b0610aed5..21029449a 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; +using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Helpers.Builders; @@ -27,13 +30,14 @@ public class CleanupServiceTests : AbstractDbTest private readonly IEventHub _messageHub = Substitute.For(); private readonly IReaderService _readerService; + public CleanupServiceTests() : base() { - Context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder(Root + "data/").Build()) + _context.Library.Add(new LibraryBuilder("Manga") + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) .Build()); - _readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), + _readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); } @@ -43,11 +47,11 @@ public class CleanupServiceTests : AbstractDbTest protected override async Task ResetDb() { - Context.Series.RemoveRange(Context.Series.ToList()); - Context.Users.RemoveRange(Context.Users.ToList()); - Context.AppUserBookmark.RemoveRange(Context.AppUserBookmark.ToList()); + _context.Series.RemoveRange(_context.Series.ToList()); + _context.Users.RemoveRange(_context.Users.ToList()); + _context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); } #endregion @@ -68,18 +72,18 @@ public class CleanupServiceTests : AbstractDbTest var s = new SeriesBuilder("Test 1").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; - Context.Series.Add(s); + _context.Series.Add(s); s = new SeriesBuilder("Test 2").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; - Context.Series.Add(s); + _context.Series.Add(s); s = new SeriesBuilder("Test 3").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg"; s.LibraryId = 1; - Context.Series.Add(s); + _context.Series.Add(s); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); @@ -102,16 +106,16 @@ public class CleanupServiceTests : AbstractDbTest var s = new SeriesBuilder("Test 1").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; - Context.Series.Add(s); + _context.Series.Add(s); s = new SeriesBuilder("Test 2").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; - Context.Series.Add(s); + _context.Series.Add(s); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); @@ -133,18 +137,18 @@ public class CleanupServiceTests : AbstractDbTest await ResetDb(); // Add 2 series with cover images - Context.Series.Add(new SeriesBuilder("Test 1") + _context.Series.Add(new SeriesBuilder("Test 1") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) + .WithChapter(new ChapterBuilder("0").WithCoverImage("v01_c01.jpg").Build()) .WithCoverImage("v01_c01.jpg") .Build()) .WithCoverImage("series_01.jpg") .WithLibraryId(1) .Build()); - Context.Series.Add(new SeriesBuilder("Test 2") + _context.Series.Add(new SeriesBuilder("Test 2") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) + .WithChapter(new ChapterBuilder("0").WithCoverImage("v01_c03.jpg").Build()) .WithCoverImage("v01_c03.jpg") .Build()) .WithCoverImage("series_03.jpg") @@ -152,9 +156,9 @@ public class CleanupServiceTests : AbstractDbTest .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteChapterCoverImages(); @@ -163,53 +167,53 @@ public class CleanupServiceTests : AbstractDbTest } #endregion - // #region DeleteTagCoverImages - // - // [Fact] - // public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles() - // { - // var filesystem = CreateFileSystem(); - // filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData("")); - // filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData("")); - // filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData("")); - // - // // Delete all Series to reset state - // await ResetDb(); - // - // // Add 2 series with cover images - // - // _context.Series.Add(new SeriesBuilder("Test 1") - // .WithMetadata(new SeriesMetadataBuilder() - // .WithCollectionTag(new AppUserCollectionBuilder("Something") - // .WithCoverImage($"{ImageService.GetCollectionTagFormat(1)}.jpg") - // .Build()) - // .Build()) - // .WithCoverImage($"{ImageService.GetSeriesFormat(1)}.jpg") - // .WithLibraryId(1) - // .Build()); - // - // _context.Series.Add(new SeriesBuilder("Test 2") - // .WithMetadata(new SeriesMetadataBuilder() - // .WithCollectionTag(new AppUserCollectionBuilder("Something") - // .WithCoverImage($"{ImageService.GetCollectionTagFormat(2)}.jpg") - // .Build()) - // .Build()) - // .WithCoverImage($"{ImageService.GetSeriesFormat(3)}.jpg") - // .WithLibraryId(1) - // .Build()); - // - // - // await _context.SaveChangesAsync(); - // var ds = new DirectoryService(Substitute.For>(), filesystem); - // var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, - // ds); - // - // await cleanupService.DeleteTagCoverImages(); - // - // Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); - // } - // - // #endregion + #region DeleteTagCoverImages + + [Fact] + public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData("")); + filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData("")); + + // Delete all Series to reset state + await ResetDb(); + + // Add 2 series with cover images + + _context.Series.Add(new SeriesBuilder("Test 1") + .WithMetadata(new SeriesMetadataBuilder() + .WithCollectionTag(new CollectionTagBuilder("Something") + .WithCoverImage($"{ImageService.GetCollectionTagFormat(1)}.jpg") + .Build()) + .Build()) + .WithCoverImage($"{ImageService.GetSeriesFormat(1)}.jpg") + .WithLibraryId(1) + .Build()); + + _context.Series.Add(new SeriesBuilder("Test 2") + .WithMetadata(new SeriesMetadataBuilder() + .WithCollectionTag(new CollectionTagBuilder("Something") + .WithCoverImage($"{ImageService.GetCollectionTagFormat(2)}.jpg") + .Build()) + .Build()) + .WithCoverImage($"{ImageService.GetSeriesFormat(3)}.jpg") + .WithLibraryId(1) + .Build()); + + + await _context.SaveChangesAsync(); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + ds); + + await cleanupService.DeleteTagCoverImages(); + + Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); + } + + #endregion #region DeleteReadingListCoverImages [Fact] @@ -223,7 +227,7 @@ public class CleanupServiceTests : AbstractDbTest // Delete all Series to reset state await ResetDb(); - Context.Users.Add(new AppUser() + _context.Users.Add(new AppUser() { UserName = "Joe", ReadingLists = new List() @@ -239,9 +243,9 @@ public class CleanupServiceTests : AbstractDbTest } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteReadingListCoverImages(); @@ -260,7 +264,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{CacheDirectory}02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); @@ -274,7 +278,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{CacheDirectory}subdir/02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); @@ -297,7 +301,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{BackupDirectory}randomfile.zip", filesystemFile); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); @@ -319,7 +323,7 @@ public class CleanupServiceTests : AbstractDbTest }); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip")); @@ -343,7 +347,7 @@ public class CleanupServiceTests : AbstractDbTest } var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); @@ -372,7 +376,7 @@ public class CleanupServiceTests : AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.True(filesystem.File.Exists($"{LogDirectory}kavita20200911.log")); @@ -385,85 +389,84 @@ public class CleanupServiceTests : AbstractDbTest [Fact] public async Task CleanupDbEntries_CleanupAbandonedChapters() { - var c = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + var c = new ChapterBuilder("0") .WithPages(1) .Build(); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(1) .WithChapter(c) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb").Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 5); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); // Delete the Chapter - Context.Chapter.Remove(c); - await UnitOfWork.CommitAsync(); - Assert.Empty(await UnitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + _context.Chapter.Remove(c); + await _unitOfWork.CommitAsync(); + Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); // NOTE: This may not be needed, the underlying DB structure seems fixed as of v0.7 await cleanupService.CleanupDbEntries(); - Assert.Empty(await UnitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); } [Fact] public async Task CleanupDbEntries_RemoveTagsWithoutSeries() { - var s = new SeriesBuilder("Test") - .WithFormat(MangaFormat.Epub) - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test LIb").Build(); - Context.Series.Add(s); - - var c = new AppUserCollection() + var c = new CollectionTag() { Title = "Test Tag", NormalizedTitle = "Test Tag".ToNormalized(), - AgeRating = AgeRating.Unknown, - Items = new List() {s} }; + var s = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadataBuilder().WithCollectionTag(c).Build()) + .Build(); + s.Library = new LibraryBuilder("Test LIb").Build(); - Context.AppUser.Add(new AppUser() + _context.Series.Add(s); + + _context.AppUser.Add(new AppUser() { - UserName = "majora2007", - Collections = new List() {c} + UserName = "majora2007" }); - await Context.SaveChangesAsync(); - var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, + await _context.SaveChangesAsync(); + + var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); // Delete the Chapter - Context.Series.Remove(s); - await UnitOfWork.CommitAsync(); + _context.Series.Remove(s); + await _unitOfWork.CommitAsync(); await cleanupService.CleanupDbEntries(); - Assert.Empty(await UnitOfWork.CollectionTagRepository.GetAllCollectionsAsync()); + Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()); } #endregion @@ -480,30 +483,24 @@ public class CleanupServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb").Build(); - Context.Series.Add(s); + _context.Series.Add(s); var user = new AppUser() { UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries", - }; - Context.AppUser.Add(user); - - await UnitOfWork.CommitAsync(); - - // Add want to read - user.WantToRead = new List() - { - new AppUserWantToRead() + WantToRead = new List() { - SeriesId = s.Id + s } }; - await UnitOfWork.CommitAsync(); + _context.AppUser.Add(user); + + await _unitOfWork.CommitAsync(); await _readerService.MarkSeriesAsRead(user, s.Id); - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); - var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); @@ -511,144 +508,13 @@ public class CleanupServiceTests : AbstractDbTest await cleanupService.CleanupWantToRead(); var wantToRead = - await UnitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, new UserParams(), new FilterDto()); + await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, new UserParams(), new FilterDto()); Assert.Equal(0, wantToRead.TotalCount); } #endregion - #region ConsolidateProgress - - [Fact] - public async Task ConsolidateProgress_ShouldRemoveDuplicates() - { - await ResetDb(); - - var s = new SeriesBuilder("Test ConsolidateProgress_ShouldRemoveDuplicates") - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPages(3) - .Build()) - .Build()) - .Build(); - - s.Library = new LibraryBuilder("Test Lib").Build(); - Context.Series.Add(s); - - var user = new AppUser() - { - UserName = "ConsolidateProgress_ShouldRemoveDuplicates", - }; - Context.AppUser.Add(user); - - await UnitOfWork.CommitAsync(); - - // Add 2 progress events - user.Progresses ??= []; - user.Progresses.Add(new AppUserProgress() - { - ChapterId = 1, - VolumeId = 1, - SeriesId = 1, - LibraryId = s.LibraryId, - PagesRead = 1, - }); - await UnitOfWork.CommitAsync(); - - // Add a duplicate with higher page number - user.Progresses.Add(new AppUserProgress() - { - ChapterId = 1, - VolumeId = 1, - SeriesId = 1, - LibraryId = s.LibraryId, - PagesRead = 3, - }); - await UnitOfWork.CommitAsync(); - - Assert.Equal(2, (await UnitOfWork.AppUserProgressRepository.GetAllProgress()).Count()); - - var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, - Substitute.For(), - new DirectoryService(Substitute.For>(), new MockFileSystem())); - - - await cleanupService.ConsolidateProgress(); - - var progress = await UnitOfWork.AppUserProgressRepository.GetAllProgress(); - - Assert.Single(progress); - Assert.True(progress.First().PagesRead == 3); - } - #endregion - - - #region EnsureChapterProgressIsCapped - - [Fact] - public async Task EnsureChapterProgressIsCapped_ShouldNormalizeProgress() - { - await ResetDb(); - - var s = new SeriesBuilder("Test CleanupWantToRead_ShouldRemoveFullyReadSeries") - .WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Completed).Build()) - .Build(); - - s.Library = new LibraryBuilder("Test LIb").Build(); - var c = new ChapterBuilder("1").WithPages(2).Build(); - c.UserProgress = new List(); - s.Volumes = new List() - { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume).WithChapter(c).Build() - }; - Context.Series.Add(s); - - var user = new AppUser() - { - UserName = "EnsureChapterProgressIsCapped", - Progresses = new List() - }; - Context.AppUser.Add(user); - - await UnitOfWork.CommitAsync(); - - await _readerService.MarkChaptersAsRead(user, s.Id, new List() {c}); - await UnitOfWork.CommitAsync(); - - var chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); - - Assert.NotNull(chapter); - Assert.Equal(2, chapter.PagesRead); - - // Update chapter to have 1 page - c.Pages = 1; - UnitOfWork.ChapterRepository.Update(c); - await UnitOfWork.CommitAsync(); - - chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); - Assert.NotNull(chapter); - Assert.Equal(2, chapter.PagesRead); - Assert.Equal(1, chapter.Pages); - - var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, - Substitute.For(), - new DirectoryService(Substitute.For>(), new MockFileSystem())); - - await cleanupService.EnsureChapterProgressIsCapped(); - chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); - - Assert.NotNull(chapter); - Assert.Equal(1, chapter.PagesRead); - - Context.AppUser.Remove(user); - await UnitOfWork.CommitAsync(); - } - #endregion - - #region CleanupBookmarks + // #region CleanupBookmarks // // [Fact] // public async Task CleanupBookmarks_LeaveAllFiles() @@ -785,5 +651,5 @@ public class CleanupServiceTests : AbstractDbTest // Assert.Equal(1, ds.FileSystem.Directory.GetDirectories($"{BookmarkDirectory}1/1/").Length); // } // - #endregion + // #endregion } diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/API.Tests/Services/CollectionTagServiceTests.cs index 3414dd86b..c06767ed1 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/API.Tests/Services/CollectionTagServiceTests.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs.Collection; +using API.DTOs.CollectionTags; using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; -using API.Services.Plus; using API.SignalR; -using Kavita.Common; +using API.Tests.Helpers; using NSubstitute; using Xunit; @@ -23,507 +20,132 @@ public class CollectionTagServiceTests : AbstractDbTest private readonly ICollectionTagService _service; public CollectionTagServiceTests() { - _service = new CollectionTagService(UnitOfWork, Substitute.For()); + _service = new CollectionTagService(_unitOfWork, Substitute.For()); } protected override async Task ResetDb() { - Context.AppUserCollection.RemoveRange(Context.AppUserCollection.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); + _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); + _context.Library.RemoveRange(_context.Library.ToList()); - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); } private async Task SeedSeries() { - if (Context.AppUserCollection.Any()) return; + if (_context.CollectionTag.Any()) return; - var s1 = new SeriesBuilder("Series 1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Mature).Build()).Build(); - var s2 = new SeriesBuilder("Series 2").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.G).Build()).Build(); - Context.Library.Add(new LibraryBuilder("Library 2", LibraryType.Manga) - .WithSeries(s1) - .WithSeries(s2) + _context.Library.Add(new LibraryBuilder("Library 2", LibraryType.Manga) + .WithSeries(new SeriesBuilder("Series 1").Build()) + .WithSeries(new SeriesBuilder("Series 2").Build()) .Build()); - var user = new AppUserBuilder("majora2007", "majora2007", Seed.DefaultThemes.First()).Build(); - user.Collections = new List() - { - new AppUserCollectionBuilder("Tag 1").WithItems(new []{s1}).Build(), - new AppUserCollectionBuilder("Tag 2").WithItems(new []{s1, s2}).WithIsPromoted(true).Build() - }; - UnitOfWork.UserRepository.Add(user); - - await UnitOfWork.CommitAsync(); + _context.CollectionTag.Add(new CollectionTagBuilder("Tag 1").Build()); + _context.CollectionTag.Add(new CollectionTagBuilder("Tag 2").WithIsPromoted(true).Build()); + await _unitOfWork.CommitAsync(); } - #region DeleteTag [Fact] - public async Task DeleteTag_ShouldDeleteTag_WhenTagExists() + public async Task TagExistsByName_ShouldFindTag() { - // Arrange await SeedSeries(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Act - var result = await _service.DeleteTag(1, user); - - // Assert - Assert.True(result); - var deletedTag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.Null(deletedTag); - Assert.Single(user.Collections); // Only one collection should remain + Assert.True(await _service.TagExistsByName("Tag 1")); + Assert.True(await _service.TagExistsByName("tag 1")); + Assert.False(await _service.TagExistsByName("tag5")); } - [Fact] - public async Task DeleteTag_ShouldReturnTrue_WhenTagDoesNotExist() - { - // Arrange - await SeedSeries(); - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Act - Try to delete a non-existent tag - var result = await _service.DeleteTag(999, user); - - // Assert - Assert.True(result); // Should return true because the tag is already "deleted" - Assert.Equal(2, user.Collections.Count); // Both collections should remain - } - - [Fact] - public async Task DeleteTag_ShouldNotAffectOtherTags() - { - // Arrange - await SeedSeries(); - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Act - var result = await _service.DeleteTag(1, user); - - // Assert - Assert.True(result); - var remainingTag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); - Assert.NotNull(remainingTag); - Assert.Equal("Tag 2", remainingTag.Title); - Assert.True(remainingTag.Promoted); - } - - #endregion - - #region UpdateTag - [Fact] public async Task UpdateTag_ShouldUpdateFields() { await SeedSeries(); - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); + _context.CollectionTag.Add(new CollectionTagBuilder("UpdateTag_ShouldUpdateFields").WithId(3).WithIsPromoted(true).Build()); + await _unitOfWork.CommitAsync(); - user.Collections.Add(new AppUserCollectionBuilder("UpdateTag_ShouldUpdateFields").WithIsPromoted(true).Build()); - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - await _service.UpdateTag(new AppUserCollectionDto() + await _service.UpdateTag(new CollectionTagDto() { Title = "UpdateTag_ShouldUpdateFields", Id = 3, Promoted = true, Summary = "Test Summary", - AgeRating = AgeRating.Unknown - }, 1); + }); - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(3); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(3); Assert.NotNull(tag); Assert.True(tag.Promoted); - Assert.False(string.IsNullOrEmpty(tag.Summary)); - } - - /// - /// UpdateTag should not change any title if non-Kavita source - /// - [Fact] - public async Task UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource() - { - await SeedSeries(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - user.Collections.Add(new AppUserCollectionBuilder("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource").WithSource(ScrobbleProvider.Mal).Build()); - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "New Title", - Id = 3, - Promoted = true, - Summary = "Test Summary", - AgeRating = AgeRating.Unknown - }, 1); - - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(3); - Assert.NotNull(tag); - Assert.Equal("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource", tag.Title); - Assert.False(string.IsNullOrEmpty(tag.Summary)); + Assert.True(!string.IsNullOrEmpty(tag.Summary)); } [Fact] - public async Task UpdateTag_ShouldThrowException_WhenTagDoesNotExist() + public async Task AddTagToSeries_ShouldAddTagToAllSeries() { - // Arrange await SeedSeries(); + var ids = new[] {1, 2}; + await _service.AddTagToSeries(await _unitOfWork.CollectionTagRepository.GetTagAsync(1, CollectionTagIncludes.SeriesMetadata), ids); - // Act & Assert - var exception = await Assert.ThrowsAsync(() => _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Non-existent Tag", - Id = 999, // Non-existent ID - Promoted = false - }, 1)); - - Assert.Equal("collection-doesnt-exist", exception.Message); + var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(ids); + Assert.Contains(metadatas.ElementAt(0).CollectionTags, t => t.Title.Equals("Tag 1")); + Assert.Contains(metadatas.ElementAt(1).CollectionTags, t => t.Title.Equals("Tag 1")); } [Fact] - public async Task UpdateTag_ShouldThrowException_WhenUserDoesNotOwnTag() - { - // Arrange - await SeedSeries(); - - // Create a second user - var user2 = new AppUserBuilder("user2", "user2", Seed.DefaultThemes.First()).Build(); - UnitOfWork.UserRepository.Add(user2); - await UnitOfWork.CommitAsync(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, // This belongs to user1 - Promoted = false - }, 2)); // User with ID 2 - - Assert.Equal("access-denied", exception.Message); - } - - [Fact] - public async Task UpdateTag_ShouldThrowException_WhenTitleIsEmpty() - { - // Arrange - await SeedSeries(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => _service.UpdateTag(new AppUserCollectionDto() - { - Title = " ", // Empty after trimming - Id = 1, - Promoted = false - }, 1)); - - Assert.Equal("collection-tag-title-required", exception.Message); - } - - [Fact] - public async Task UpdateTag_ShouldThrowException_WhenTitleAlreadyExists() - { - // Arrange - await SeedSeries(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 2", // Already exists - Id = 1, // Trying to rename Tag 1 to Tag 2 - Promoted = false - }, 1)); - - Assert.Equal("collection-tag-duplicate", exception.Message); - } - - [Fact] - public async Task UpdateTag_ShouldUpdateCoverImageSettings() - { - // Arrange - await SeedSeries(); - - // Act - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, - CoverImageLocked = true - }, 1); - - // Assert - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.True(tag.CoverImageLocked); - - // Now test unlocking the cover image - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, - CoverImageLocked = false - }, 1); - - tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.False(tag.CoverImageLocked); - Assert.Equal(string.Empty, tag.CoverImage); - } - - [Fact] - public async Task UpdateTag_ShouldAllowPromoteForAdminRole() - { - // Arrange - await SeedSeries(); - - // Setup a user with admin role - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - await AddUserWithRole(user.Id, PolicyConstants.AdminRole); - - - // Act - Try to promote a tag that wasn't previously promoted - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, - Promoted = true - }, 1); - - // Assert - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.True(tag.Promoted); - } - - [Fact] - public async Task UpdateTag_ShouldAllowPromoteForPromoteRole() - { - // Arrange - await SeedSeries(); - - // Setup a user with promote role - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Mock to return promote role for the user - await AddUserWithRole(user.Id, PolicyConstants.PromoteRole); - - // Act - Try to promote a tag that wasn't previously promoted - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, - Promoted = true - }, 1); - - // Assert - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.True(tag.Promoted); - } - - [Fact] - public async Task UpdateTag_ShouldNotChangePromotion_WhenUserHasNoPermission() - { - // Arrange - await SeedSeries(); - - // Setup a user with no special roles - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Act - Try to promote a tag without proper role - await _service.UpdateTag(new AppUserCollectionDto() - { - Title = "Tag 1", - Id = 1, - Promoted = true - }, 1); - - // Assert - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.False(tag.Promoted); // Should remain unpromoted - } - #endregion - - - #region RemoveTagFromSeries - - [Fact] - public async Task RemoveTagFromSeries_RemoveSeriesFromTag() + public async Task RemoveTagFromSeries_ShouldRemoveMultiple() { await SeedSeries(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Tag 2 has 2 series - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); - Assert.NotNull(tag); - - await _service.RemoveTagFromSeries(tag, new[] {1}); - var userCollections = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.Equal(2, userCollections!.Collections.Count); - Assert.Single(tag.Items); - Assert.Equal(2, tag.Items.First().Id); - } - - /// - /// Ensure the rating of the tag updates after a series change - /// - [Fact] - public async Task RemoveTagFromSeries_RemoveSeriesFromTag_UpdatesRating() - { - await SeedSeries(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Tag 2 has 2 series - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); - Assert.NotNull(tag); + var ids = new[] {1, 2}; + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(2, CollectionTagIncludes.SeriesMetadata); + await _service.AddTagToSeries(tag, ids); await _service.RemoveTagFromSeries(tag, new[] {1}); - Assert.Equal(AgeRating.G, tag.AgeRating); + var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(new[] {1}); + + Assert.Single(metadatas); + Assert.Empty(metadatas.First().CollectionTags); + Assert.NotEmpty(await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(new[] {2})); } - /// - /// Should remove the tag when there are no items left on the tag - /// [Fact] - public async Task RemoveTagFromSeries_RemoveSeriesFromTag_DeleteTagWhenNoSeriesLeft() + public async Task GetTagOrCreate_ShouldReturnNewTag() { await SeedSeries(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); - Assert.NotNull(user); - - // Tag 1 has 1 series - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await _service.GetTagOrCreate(0, "GetTagOrCreate_ShouldReturnNewTag"); Assert.NotNull(tag); + Assert.Equal(0, tag.Id); + } + + [Fact] + public async Task GetTagOrCreate_ShouldReturnExistingTag() + { + await SeedSeries(); + var tag = await _service.GetTagOrCreate(1, "Some new tag"); + Assert.NotNull(tag); + Assert.Equal(1, tag.Id); + Assert.Equal("Tag 1", tag.Title); + } + + [Fact] + public async Task RemoveTagsWithoutSeries_ShouldRemoveAbandonedEntries() + { + await SeedSeries(); + // Setup a tag with one series + var tag = await _service.GetTagOrCreate(0, "Tag with a series"); + await _unitOfWork.CommitAsync(); + + var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(new[] {1}); + tag.SeriesMetadatas.Add(metadatas.First()); + var tagId = tag.Id; + await _unitOfWork.CommitAsync(); + + // Validate it doesn't remove tags it shouldn't + await _service.RemoveTagsWithoutSeries(); + Assert.NotNull(await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId)); await _service.RemoveTagFromSeries(tag, new[] {1}); - var tag2 = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.Null(tag2); + + // Validate it does remove tags it should + await _service.RemoveTagsWithoutSeries(); + Assert.Null(await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId)); } - - [Fact] - public async Task RemoveTagFromSeries_ShouldReturnFalse_WhenTagIsNull() - { - // Act - var result = await _service.RemoveTagFromSeries(null, [1]); - - // Assert - Assert.False(result); - } - - [Fact] - public async Task RemoveTagFromSeries_ShouldHandleEmptySeriesIdsList() - { - // Arrange - await SeedSeries(); - - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - var initialItemCount = tag.Items.Count; - - // Act - var result = await _service.RemoveTagFromSeries(tag, Array.Empty()); - - // Assert - Assert.True(result); - tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed - } - - [Fact] - public async Task RemoveTagFromSeries_ShouldHandleNonExistentSeriesIds() - { - // Arrange - await SeedSeries(); - - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - var initialItemCount = tag.Items.Count; - - // Act - Try to remove a series that doesn't exist in the tag - var result = await _service.RemoveTagFromSeries(tag, [999]); - - // Assert - Assert.True(result); - tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed - } - - [Fact] - public async Task RemoveTagFromSeries_ShouldHandleNullItemsList() - { - // Arrange - await SeedSeries(); - - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.NotNull(tag); - - // Force null items list - tag.Items = null; - UnitOfWork.CollectionTagRepository.Update(tag); - await UnitOfWork.CommitAsync(); - - // Act - var result = await _service.RemoveTagFromSeries(tag, [1]); - - // Assert - Assert.True(result); - // The tag should not be removed since the items list was null, not empty - var tagAfter = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); - Assert.Null(tagAfter); - } - - [Fact] - public async Task RemoveTagFromSeries_ShouldUpdateAgeRating_WhenMultipleSeriesRemain() - { - // Arrange - await SeedSeries(); - - // Add a third series with a different age rating - var s3 = new SeriesBuilder("Series 3").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.PG).Build()).Build(); - Context.Library.First().Series.Add(s3); - await UnitOfWork.CommitAsync(); - - // Add series 3 to tag 2 - var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); - Assert.NotNull(tag); - tag.Items.Add(s3); - UnitOfWork.CollectionTagRepository.Update(tag); - await UnitOfWork.CommitAsync(); - - // Act - Remove the series with Mature rating - await _service.RemoveTagFromSeries(tag, new[] {1}); - - // Assert - tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); - Assert.NotNull(tag); - Assert.Equal(2, tag.Items.Count); - - // The age rating should be updated to the highest remaining rating (PG) - Assert.Equal(AgeRating.PG, tag.AgeRating); - } - - - #endregion - } diff --git a/API.Tests/Services/CoverDbServiceTests.cs b/API.Tests/Services/CoverDbServiceTests.cs deleted file mode 100644 index 93217c3b5..000000000 --- a/API.Tests/Services/CoverDbServiceTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.IO; -using System.IO.Abstractions; -using System.Reflection; -using System.Threading.Tasks; -using API.Constants; -using API.Entities.Enums; -using API.Extensions; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; -using EasyCaching.Core; -using Kavita.Common; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class CoverDbServiceTests : AbstractDbTest -{ - private readonly DirectoryService _directoryService; - private readonly IEasyCachingProviderFactory _cacheFactory = Substitute.For(); - private readonly ICoverDbService _coverDbService; - - private static readonly string FaviconPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Favicons"); - /// - /// Path to download files temp to. Should be empty after each test. - /// - private static readonly string TempPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Temp"); - - public CoverDbServiceTests() - { - _directoryService = new DirectoryService(Substitute.For>(), CreateFileSystem()); - var imageService = new ImageService(Substitute.For>(), _directoryService); - - _coverDbService = new CoverDbService(Substitute.For>(), _directoryService, _cacheFactory, - Substitute.For(), imageService, UnitOfWork, Substitute.For()); - } - - protected override Task ResetDb() - { - throw new System.NotImplementedException(); - } - - - #region Download Favicon - - /// - /// I cannot figure out how to test this code due to the reliance on the _directoryService.FaviconDirectory and not being - /// able to redirect it to the real filesystem. - /// - public async Task DownloadFaviconAsync_ShouldDownloadAndMatchExpectedFavicon() - { - // Arrange - var testUrl = "https://anilist.co/anime/6205/Kmpfer/"; - var encodeFormat = EncodeFormat.WEBP; - var expectedFaviconPath = Path.Combine(FaviconPath, "anilist.co.webp"); - - // Ensure TempPath exists - _directoryService.ExistOrCreate(TempPath); - - var baseUrl = "https://anilist.co"; - - // Ensure there is no cache result for this URL - var provider = Substitute.For(); - provider.GetAsync(baseUrl).Returns(new CacheValue(null, false)); - _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon).Returns(provider); - - - // // Replace favicon directory with TempPath - // var directoryService = (DirectoryService)_directoryService; - // directoryService.FaviconDirectory = TempPath; - - // Hack: Swap FaviconDirectory with TempPath for ability to download real files - typeof(DirectoryService) - .GetField("FaviconDirectory", BindingFlags.NonPublic | BindingFlags.Instance) - ?.SetValue(_directoryService, TempPath); - - - // Act - var resultFilename = await _coverDbService.DownloadFaviconAsync(testUrl, encodeFormat); - var actualFaviconPath = Path.Combine(TempPath, resultFilename); - - // Assert file exists - Assert.True(File.Exists(actualFaviconPath), "Downloaded favicon does not exist in temp path"); - - // Load and compare similarity - - var similarity = expectedFaviconPath.CalculateSimilarity(actualFaviconPath); // Assuming you have this extension - Assert.True(similarity > 0.9f, $"Image similarity too low: {similarity}"); - } - - [Fact] - public async Task DownloadFaviconAsync_ShouldThrowKavitaException_WhenPreviouslyFailedUrlExistsInCache() - { - // Arrange - var testUrl = "https://example.com"; - var encodeFormat = EncodeFormat.WEBP; - - var provider = Substitute.For(); - provider.GetAsync(Arg.Any()) - .Returns(new CacheValue(string.Empty, true)); // Simulate previous failure - - _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon).Returns(provider); - - // Act & Assert - await Assert.ThrowsAsync(() => - _coverDbService.DownloadFaviconAsync(testUrl, encodeFormat)); - } - - #endregion - - -} diff --git a/API.Tests/Services/DeviceServiceTests.cs b/API.Tests/Services/DeviceServiceTests.cs index cbcf70f82..1d021c76d 100644 --- a/API.Tests/Services/DeviceServiceTests.cs +++ b/API.Tests/Services/DeviceServiceTests.cs @@ -18,13 +18,13 @@ public class DeviceServiceDbTests : AbstractDbTest public DeviceServiceDbTests() : base() { - _deviceService = new DeviceService(UnitOfWork, _logger, Substitute.For()); + _deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For()); } protected override async Task ResetDb() { - Context.Users.RemoveRange(Context.Users.ToList()); - await UnitOfWork.CommitAsync(); + _context.Users.RemoveRange(_context.Users.ToList()); + await _unitOfWork.CommitAsync(); } @@ -39,8 +39,8 @@ public class DeviceServiceDbTests : AbstractDbTest Devices = new List() }; - Context.Users.Add(user); - await UnitOfWork.CommitAsync(); + _context.Users.Add(user); + await _unitOfWork.CommitAsync(); var device = await _deviceService.Create(new CreateDeviceDto() { @@ -62,8 +62,8 @@ public class DeviceServiceDbTests : AbstractDbTest Devices = new List() }; - Context.Users.Add(user); - await UnitOfWork.CommitAsync(); + _context.Users.Add(user); + await _unitOfWork.CommitAsync(); var device = await _deviceService.Create(new CreateDeviceDto() { diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index c5216bebf..22c9bd269 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,30 +1,20 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using API.Services; -using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Services; -public class DirectoryServiceTests: AbstractFsTest +public class DirectoryServiceTests { private readonly ILogger _logger = Substitute.For>(); - private readonly ITestOutputHelper _testOutputHelper; - - public DirectoryServiceTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } #region TraverseTreeParallelForEach @@ -382,16 +372,9 @@ public class DirectoryServiceTests: AbstractFsTest #endregion #region IsDriveMounted - // The root directory (/) is always mounted on non windows [Fact] public void IsDriveMounted_DriveIsNotMounted() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _testOutputHelper.WriteLine("Skipping test on non Windows platform"); - return; - } - const string testDirectory = "c:/manga/"; var fileSystem = new MockFileSystem(); fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc")); @@ -403,12 +386,6 @@ public class DirectoryServiceTests: AbstractFsTest [Fact] public void IsDriveMounted_DriveIsMounted() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _testOutputHelper.WriteLine("Skipping test on non Windows platform"); - return; - } - const string testDirectory = "c:/manga/"; var fileSystem = new MockFileSystem(); fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc")); @@ -744,54 +721,6 @@ public class DirectoryServiceTests: AbstractFsTest #endregion - #region FindLowestDirectoriesFromFiles - - [Theory] - [InlineData(new [] {"C:/Manga/"}, - new [] {"C:/Manga/Love Hina/Vol. 01.cbz"}, - "C:/Manga/Love Hina")] - [InlineData(new [] {"C:/Manga/"}, - new [] {"C:/Manga/Romance/Love Hina/Vol. 01.cbz"}, - "C:/Manga/Romance/Love Hina")] - [InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/Dir 2/"}, - new [] {"C:/Manga/Dir 1/Love Hina/Vol. 01.cbz"}, - "C:/Manga/Dir 1/Love Hina")] - [InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/"}, - new [] {"D:/Manga/Love Hina/Vol. 01.cbz", "D:/Manga/Vol. 01.cbz"}, - null)] - [InlineData(new [] {@"C:\mount\drive\Library\Test Library\Comics\"}, - new [] {@"C:\mount\drive\Library\Test Library\Comics\Bruce Lee (1994)\Bruce Lee #001 (1994).cbz"}, - @"C:/mount/drive/Library/Test Library/Comics/Bruce Lee (1994)")] - [InlineData(new [] {"C:/Manga/"}, - new [] {"C:/Manga/Love Hina/Vol. 01.cbz", "C:/Manga/Love Hina/Specials/Sp01.cbz"}, - "C:/Manga/Love Hina")] - [InlineData(new [] {"/manga"}, - new [] {"/manga/Love Hina/Vol. 01.cbz", "/manga/Love Hina/Specials/Sp01.cbz"}, - "/manga/Love Hina")] - [InlineData(new [] {"/manga"}, - new [] {"/manga/Love Hina/Hina/Vol. 01.cbz", "/manga/Love Hina/Specials/Sp01.cbz"}, - "/manga/Love Hina")] - [InlineData(new [] {"/manga"}, - new [] {"/manga/Dress Up Darling/Dress Up Darling Ch 01.cbz", "/manga/Dress Up Darling/Dress Up Darling/Dress Up Darling Vol 01.cbz"}, - "/manga/Dress Up Darling")] - public void FindLowestDirectoriesFromFilesTest(string[] rootDirectories, string[] files, string expectedDirectory) - { - var fileSystem = new MockFileSystem(); - foreach (var directory in rootDirectories) - { - fileSystem.AddDirectory(directory); - } - foreach (var f in files) - { - fileSystem.AddFile(f, new MockFileData("")); - } - var ds = new DirectoryService(Substitute.For>(), fileSystem); - - var actual = ds.FindLowestDirectoriesFromFiles(rootDirectories, files); - Assert.Equal(expectedDirectory, actual); - } - - #endregion #region GetFoldersTillRoot [Theory] @@ -922,14 +851,12 @@ public class DirectoryServiceTests: AbstractFsTest #region GetHumanReadableBytes [Theory] - [InlineData(1200, 1.17, " KB")] - [InlineData(1, 1, " B")] - [InlineData(10000000, 9.54, " MB")] - [InlineData(10000000000, 9.31, " GB")] - public void GetHumanReadableBytesTest(long bytes, float number, string suffix) + [InlineData(1200, "1.17 KB")] + [InlineData(1, "1 B")] + [InlineData(10000000, "9.54 MB")] + [InlineData(10000000000, "9.31 GB")] + public void GetHumanReadableBytesTest(long bytes, string expected) { - // GetHumanReadableBytes is user facing, should be in CultureInfo.CurrentCulture - var expected = number.ToString(CultureInfo.CurrentCulture) + suffix; Assert.Equal(expected, DirectoryService.GetHumanReadableBytes(bytes)); } #endregion @@ -951,9 +878,8 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var globMatcher = new GlobMatcher(); - globMatcher.AddExclude("*.*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + + var allFiles = ds.ScanFiles("C:/Data/"); Assert.Empty(allFiles); @@ -977,9 +903,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var globMatcher = new GlobMatcher(); - globMatcher.AddExclude("**/Accel World/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/"); Assert.Single(allFiles); // Ignore files are not counted in files, only valid extensions @@ -1008,10 +932,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var globMatcher = new GlobMatcher(); - globMatcher.AddExclude("**/Accel World/*"); - globMatcher.AddExclude("**/ArtBooks/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/"); Assert.Equal(2, allFiles.Count); // Ignore files are not counted in files, only valid extensions @@ -1035,7 +956,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); + var allFiles = ds.ScanFiles("C:/Data/"); Assert.Equal(5, allFiles.Count); @@ -1065,14 +986,11 @@ public class DirectoryServiceTests: AbstractFsTest #region GetParentDirectory [Theory] - [InlineData(@"file.txt", "")] - [InlineData(@"folder/file.txt", "folder")] - [InlineData(@"folder/subfolder/file.txt", "folder/subfolder")] + [InlineData(@"C:/file.txt", "C:/")] + [InlineData(@"C:/folder/file.txt", "C:/folder")] + [InlineData(@"C:/folder/subfolder/file.txt", "C:/folder/subfolder")] public void GetParentDirectoryName_ShouldFindParentOfFiles(string path, string expected) { - path = Root + path; - expected = Root + expected; - var fileSystem = new MockFileSystem(new Dictionary { { path, new MockFileData(string.Empty)} @@ -1082,14 +1000,11 @@ public class DirectoryServiceTests: AbstractFsTest Assert.Equal(expected, ds.GetParentDirectoryName(path)); } [Theory] - [InlineData(@"folder", "")] - [InlineData(@"folder/subfolder", "folder")] - [InlineData(@"folder/subfolder/another", "folder/subfolder")] + [InlineData(@"C:/folder", "C:/")] + [InlineData(@"C:/folder/subfolder", "C:/folder")] + [InlineData(@"C:/folder/subfolder/another", "C:/folder/subfolder")] public void GetParentDirectoryName_ShouldFindParentOfDirectories(string path, string expected) { - path = Root + path; - expected = Root + expected; - var fileSystem = new MockFileSystem(); fileSystem.AddDirectory(path); diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs deleted file mode 100644 index 973b7c6df..000000000 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ /dev/null @@ -1,3198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using Hangfire; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -/// -/// Given these rely on Kavita+, this will not have any [Fact]/[Theory] on them and must be manually checked -/// -public class ExternalMetadataServiceTests : AbstractDbTest -{ - private readonly ExternalMetadataService _externalMetadataService; - private readonly Dictionary _genreLookup = new Dictionary(); - private readonly Dictionary _tagLookup = new Dictionary(); - private readonly Dictionary _personLookup = new Dictionary(); - - - public ExternalMetadataServiceTests() - { - // Set up Hangfire to use in-memory storage for testing - GlobalConfiguration.Configuration.UseInMemoryStorage(); - - _externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For>(), - Mapper, Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For(), Substitute.For()); - } - - #region Gloabl - - [Fact] - public async Task Off_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = false; - metadataSettings.EnableSummary = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "Test" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(string.Empty, postSeries.Metadata.Summary); - } - - #endregion - - #region Summary - - [Fact] - public async Task Summary_NoExisting_Off_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableSummary = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "Test" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(string.Empty, postSeries.Metadata.Summary); - } - - [Fact] - public async Task Summary_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableSummary = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "Test" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); - Assert.Equal(series.Metadata.Summary, postSeries.Metadata.Summary); - } - - [Fact] - public async Task Summary_Existing_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithSummary("This summary is not locked") - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableSummary = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "This should not write" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); - Assert.Equal("This summary is not locked", postSeries.Metadata.Summary); - } - - [Fact] - public async Task Summary_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithSummary("This summary is not locked", true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableSummary = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "This should not write" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); - Assert.Equal("This summary is not locked", postSeries.Metadata.Summary); - } - - [Fact] - public async Task Summary_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Summary"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithSummary("This summary is not locked", true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableSummary = true; - metadataSettings.Overrides = [MetadataSettingField.Summary]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Summary = "This should write" - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); - Assert.Equal("This should write", postSeries.Metadata.Summary); - } - - - #endregion - - #region Release Year - - [Fact] - public async Task ReleaseYear_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Release Year"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableStartDate = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - StartDate = DateTime.UtcNow - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(0, postSeries.Metadata.ReleaseYear); - } - - [Fact] - public async Task ReleaseYear_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Release Year"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableStartDate = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - StartDate = DateTime.UtcNow - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(DateTime.UtcNow.Year, postSeries.Metadata.ReleaseYear); - } - - [Fact] - public async Task ReleaseYear_Existing_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Release Year"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithReleaseYear(1990) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableStartDate = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - StartDate = DateTime.UtcNow - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(1990, postSeries.Metadata.ReleaseYear); - } - - [Fact] - public async Task ReleaseYear_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Release Year"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithReleaseYear(1990, true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableStartDate = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - StartDate = DateTime.UtcNow - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(1990, postSeries.Metadata.ReleaseYear); - } - - [Fact] - public async Task ReleaseYear_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Release Year"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithReleaseYear(1990, true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableStartDate = true; - metadataSettings.Overrides = [MetadataSettingField.StartDate]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - StartDate = DateTime.UtcNow - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(DateTime.UtcNow.Year, postSeries.Metadata.ReleaseYear); - } - - #endregion - - #region LocalizedName - - [Fact] - public async Task LocalizedName_NoExisting_Off_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedNameAllowEmpty(string.Empty) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください", "Kimchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(string.Empty, postSeries.LocalizedName); - } - - [Fact] - public async Task LocalizedName_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedNameAllowEmpty(string.Empty) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください", "Kimchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal("Kimchi", postSeries.LocalizedName); - } - - [Fact] - public async Task LocalizedName_Existing_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedName("Localized Name here") - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください", "Kimchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal("Localized Name here", postSeries.LocalizedName); - } - - [Fact] - public async Task LocalizedName_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedName("Localized Name here", true) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください", "Kimchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal("Localized Name here", postSeries.LocalizedName); - } - - [Fact] - public async Task LocalizedName_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedName("Localized Name here", true) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = true; - metadataSettings.Overrides = [MetadataSettingField.LocalizedName]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください", "Kimchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal("Kimchi", postSeries.LocalizedName); - } - - [Fact] - public async Task LocalizedName_OnlyNonEnglishSynonyms_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Localized Name"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithLocalizedNameAllowEmpty(string.Empty) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableLocalizedName = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Synonyms = [seriesName, "設定しないでください"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.True(string.IsNullOrEmpty(postSeries.LocalizedName)); - } - - #endregion - - #region Publication Status - - [Fact] - public async Task PublicationStatus_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.OnGoing, postSeries.Metadata.PublicationStatus); - } - - [Fact] - public async Task PublicationStatus_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); - } - - [Fact] - public async Task PublicationStatus_Existing_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPublicationStatus(PublicationStatus.Hiatus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); - } - - [Fact] - public async Task PublicationStatus_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPublicationStatus(PublicationStatus.Hiatus, true) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.Hiatus, postSeries.Metadata.PublicationStatus); - } - - [Fact] - public async Task PublicationStatus_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPublicationStatus(PublicationStatus.Hiatus, true) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = true; - metadataSettings.Overrides = [MetadataSettingField.PublicationStatus]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); - } - - [Fact] - public async Task PublicationStatus_Existing_CorrectState_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Publication Status"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPublicationStatus(PublicationStatus.Hiatus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePublicationStatus = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Volumes = 2 - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(PublicationStatus.Ended, postSeries.Metadata.PublicationStatus); - } - - - [Fact] - public void IsSeriesCompleted_ExactMatch() - { - const string seriesName = "Test - Exact Match"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(5) - .WithTotalCount(5) - .Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 5, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - } - - [Fact] - public void IsSeriesCompleted_Volumes_DecimalVolumes() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder("2.5").WithNumber(2.5f).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes decimal volume 2.5 - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.True(result); - Assert.Equal(3, series.Metadata.MaxCount); - Assert.Equal(3, series.Metadata.TotalCount); - } - - /// - /// This is validating that we get a completed even though we have a special chapter and AL doesn't count it - /// - [Fact] - public void IsSeriesCompleted_Volumes_HasSpecialAndDecimal_ExternalNoSpecial() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("1.5").WithNumber(1.5f).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes volume 1.5, but not the special - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.True(result); - Assert.Equal(3, series.Metadata.MaxCount); - Assert.Equal(3, series.Metadata.TotalCount); - } - - /// - /// This unit test also illustrates the bug where you may get a false positive if you had Volumes 1,2, and 2.1. While - /// missing volume 3. With the external metadata expecting non-decimal volumes. - /// i.e. it would fail if we only had one decimal volume - /// - [Fact] - public void IsSeriesCompleted_Volumes_TooManyDecimalVolumes() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder("2.1").WithNumber(2.1f).Build()) - .WithVolume(new VolumeBuilder("2.2").WithNumber(2.2f).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes no special or decimals. There are 3 volumes. And we're missing volume 3 - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.False(result); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_GEQChapterCheck() - { - // We own 11 chapters, the external metadata expects 10 - const string seriesName = "Test - Chapter MaxCount, no volumes"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(11) - .WithTotalCount(10) - .Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - Assert.Equal(11, series.Metadata.TotalCount); - Assert.Equal(11, series.Metadata.MaxCount); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_IncludeAllChaptersCheck() - { - const string seriesName = "Test - Chapter Count"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(7) - .WithTotalCount(10) - .Build()) - .Build(); - - var chapters = new List - { - new ChapterBuilder("0").Build(), - new ChapterBuilder("2").Build(), - new ChapterBuilder("3").Build(), - new ChapterBuilder("4").Build(), - new ChapterBuilder("5").Build(), - new ChapterBuilder("6").Build(), - new ChapterBuilder("7").Build(), - new ChapterBuilder("7.1").Build(), - new ChapterBuilder("7.2").Build(), - new ChapterBuilder("7.3").Build() - }; - // External metadata includes prologues (0) and extra's (7.X) - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - Assert.Equal(10, series.Metadata.TotalCount); - Assert.Equal(10, series.Metadata.MaxCount); - } - - [Fact] - public void IsSeriesCompleted_NotEnoughVolumes() - { - const string seriesName = "Test - Incomplete Volume"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(5) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 5 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.False(result); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_NotEnoughChapters() - { - const string seriesName = "Test - Incomplete Chapter"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(5) - .WithTotalCount(8) - .Build()) - .Build(); - - var chapters = new List - { - new ChapterBuilder("1").Build(), - new ChapterBuilder("2").Build(), - new ChapterBuilder("3").Build() - }; - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.False(result); - } - - - #endregion - - #region Age Rating - - [Fact] - public async Task AgeRating_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Age Rating"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.AgeRatingMappings = new Dictionary() - { - {"Ecchi", AgeRating.Teen}, // Genre - {"H", AgeRating.R18Plus}, // Tag - }; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); - } - - [Fact] - public async Task AgeRating_ExistingHigher_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Age Rating"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithAgeRating(AgeRating.Mature) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.AgeRatingMappings = new Dictionary() - { - {"Ecchi", AgeRating.Teen}, // Genre - {"H", AgeRating.R18Plus}, // Tag - }; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(AgeRating.Mature, postSeries.Metadata.AgeRating); - } - - [Fact] - public async Task AgeRating_ExistingLower_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Age Rating"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithAgeRating(AgeRating.Everyone) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.AgeRatingMappings = new Dictionary() - { - {"Ecchi", AgeRating.Teen}, // Genre - {"H", AgeRating.R18Plus}, // Tag - }; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); - } - - [Fact] - public async Task AgeRating_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Age Rating"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithAgeRating(AgeRating.Everyone, true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.AgeRatingMappings = new Dictionary() - { - {"Ecchi", AgeRating.Teen}, // Genre - {"H", AgeRating.R18Plus}, // Tag - }; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(AgeRating.Everyone, postSeries.Metadata.AgeRating); - } - - [Fact] - public async Task AgeRating_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Age Rating"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithAgeRating(AgeRating.Everyone, true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.Overrides = [MetadataSettingField.AgeRating]; - metadataSettings.AgeRatingMappings = new Dictionary() - { - {"Ecchi", AgeRating.Teen}, // Genre - {"H", AgeRating.R18Plus}, // Tag - }; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); - } - - #endregion - - #region Genres - - [Fact] - public async Task Genres_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal([], postSeries.Metadata.Genres); - } - - [Fact] - public async Task Genres_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); - } - - [Fact] - public async Task Genres_Existing_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithGenre(_genreLookup["Action"]) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); - } - - [Fact] - public async Task Genres_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithGenre(_genreLookup["Action"], true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Action"], postSeries.Metadata.Genres.Select(g => g.Title)); - } - - [Fact] - public async Task Genres_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithGenre(_genreLookup["Action"], true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - metadataSettings.Overrides = [MetadataSettingField.Genres]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); - } - - #endregion - - #region Tags - - [Fact] - public async Task Tags_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal([], postSeries.Metadata.Tags); - } - - [Fact] - public async Task Tags_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Boxing"], postSeries.Metadata.Tags.Select(t => t.Title)); - } - - [Fact] - public async Task Tags_Existing_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithTag(_tagLookup["H"], true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["H"], postSeries.Metadata.Tags.Select(t => t.Title)); - } - - [Fact] - public async Task Tags_Existing_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithTag(_tagLookup["H"], true) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.Overrides = [MetadataSettingField.Tags]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Boxing"], postSeries.Metadata.Tags.Select(t => t.Title)); - } - - #endregion - - #region People - Writers/Artists - - [Fact] - public async Task People_Writer_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal([], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer)); - } - - [Fact] - public async Task People_Writer_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["John Doe"], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).Select(p => p.Person.Name)); - } - - [Fact] - public async Task People_Writer_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Writer) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - [Fact] - public async Task People_Writer_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Writer) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe", "Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - Assert.True( postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .FirstOrDefault(p => p.Person.Name == "John Doe")!.KavitaPlusConnection); - } - - [Fact] - public async Task People_Writer_Locked_Override_ReverseNamingMatch_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Writer) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = false; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("Twowheeler", "Johnny", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - [Fact] - public async Task People_Writer_Locked_Override_PersonRoleNotSet_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Writer) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = []; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - - [Fact] - public async Task People_Writer_OverrideReMatchDeletesOld_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Writer/Artists"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe", "Story")] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe 2", "Story")] - }, 1); - - postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe 2"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - #endregion - - #region People Alias - - [Fact] - public async Task PeopleAliasing_AddAsAlias() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - Context.Person.Add(new PersonBuilder("John Doe").Build()); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("Doe", "John", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Single(allWriters); - - var johnDoe = allWriters[0].Person; - - Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); - } - - [Fact] - public async Task PeopleAliasing_AddOnAlias() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - Context.Person.Add(new PersonBuilder("John Doe").WithAlias("Doe John").Build()); - - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("Doe", "John", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Single(allWriters); - - var johnDoe = allWriters[0].Person; - - Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); - } - - [Fact] - public async Task PeopleAliasing_DontAddAsAlias_SameButNotSwitched() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe Doe", "Story"), CreateStaff("Doe", "John Doe", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Equal(2, allWriters.Count); - } - - #endregion - - #region People - Characters - - [Fact] - public async Task People_Character_NoExisting_Off_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = false; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal([], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character)); - } - - [Fact] - public async Task People_Character_NoExisting_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["John Doe"], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character).Select(p => p.Person.Name)); - } - - [Fact] - public async Task People_Character_Locked_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Character) - .Build()) - .Build(); - series.Metadata.CharacterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - [Fact] - public async Task People_Character_Locked_Override_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Character) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Character]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe", "Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - Assert.True( postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .FirstOrDefault(p => p.Person.Name == "John Doe")!.KavitaPlusConnection); - } - - [Fact] - public async Task People_Character_Locked_Override_ReverseNamingNoMatch_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Character) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = false; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Character]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("Twowheeler", "Johnny", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler", "Twowheeler Johnny"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - [Fact] - public async Task People_Character_Locked_Override_PersonRoleNotSet_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(_personLookup["Johnny Twowheeler"], PersonRole.Character) - .Build()) - .Build(); - series.Metadata.WriterLocked = true; - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = []; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - - [Fact] - public async Task People_Character_OverrideReMatchDeletesOld_Modification() - { - await ResetDb(); - - const string seriesName = "Test - People - Character"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Character]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe", CharacterRole.Main)] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Characters = [CreateCharacter("John", "Doe 2", CharacterRole.Main)] - }, 1); - - postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[]{"John Doe 2"}.OrderBy(s => s), - postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) - .Select(p => p.Person.Name) - .OrderBy(s => s)); - } - - #endregion - - #region Series Cover - // Not sure how to test this - #endregion - - #region Relationships - - // Not enabled - - // Non-Sequel - - [Fact] - public async Task Relationships_NonSequel() - { - await ResetDb(); - - const string seriesName = "Test - Relationships Side Story"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .WithExternalMetadata(new ExternalSeriesMetadata() - { - AniListId = 10 - }) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Relations = [new SeriesRelationship() - { - Relation = RelationKind.SideStory, - SeriesName = new ALMediaTitle() - { - PreferredTitle = series2.Name, - EnglishTitle = null, - NativeTitle = series2.Name, - RomajiTitle = series2.Name, - }, - AniListId = 10, - PlusMediaFormat = PlusMediaFormat.Manga - }] - }, 1); - - // Repull Series and validate what is overwritten - var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sourceSeries); - Assert.Single(sourceSeries.Relations); - Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); - } - - [Fact] - public async Task Relationships_NonSequel_LocalizedName() - { - await ResetDb(); - - const string seriesName = "Test - Relationships Side Story"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") - .WithLibraryId(1) - .WithLocalizedName("School bus") - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Relations = [new SeriesRelationship() - { - Relation = RelationKind.SideStory, - SeriesName = new ALMediaTitle() - { - PreferredTitle = "School bus", - EnglishTitle = null, - NativeTitle = series2.Name, - RomajiTitle = series2.Name, - }, - AniListId = 10, - PlusMediaFormat = PlusMediaFormat.Manga - }] - }, 1); - - // Repull Series and validate what is overwritten - var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sourceSeries); - Assert.Single(sourceSeries.Relations); - Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); - } - - // Non-Sequel with no match due to Format difference - [Fact] - public async Task Relationships_NonSequel_FormatDifference() - { - await ResetDb(); - - const string seriesName = "Test - Relationships Side Story"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") - .WithLibraryId(1) - .WithLocalizedName("School bus") - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Relations = [new SeriesRelationship() - { - Relation = RelationKind.SideStory, - SeriesName = new ALMediaTitle() - { - PreferredTitle = "School bus", - EnglishTitle = null, - NativeTitle = series2.Name, - RomajiTitle = series2.Name, - }, - AniListId = 10, - PlusMediaFormat = PlusMediaFormat.Book - }] - }, 1); - - // Repull Series and validate what is overwritten - var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sourceSeries); - Assert.Empty(sourceSeries.Relations); - } - - // Non-Sequel existing relationship with new link, both exist - [Fact] - public async Task Relationships_NonSequel_ExistingLink_DifferentType_BothExist() - { - await ResetDb(); - - var existingRelationshipSeries = new SeriesBuilder("Existing") - .WithLibraryId(1) - .Build(); - Context.Series.Attach(existingRelationshipSeries); - await Context.SaveChangesAsync(); - - const string seriesName = "Test - Relationships Side Story"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithRelationship(existingRelationshipSeries.Id, RelationKind.Annual) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .WithExternalMetadata(new ExternalSeriesMetadata() - { - AniListId = 10 - }) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Relations = [new SeriesRelationship() - { - Relation = RelationKind.SideStory, - SeriesName = new ALMediaTitle() - { - PreferredTitle = series2.Name, - EnglishTitle = null, - NativeTitle = series2.Name, - RomajiTitle = series2.Name, - }, - PlusMediaFormat = PlusMediaFormat.Manga - }] - }, 2); - - // Repull Series and validate what is overwritten - var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sourceSeries); - Assert.Equal(seriesName, sourceSeries.Name); - - Assert.Contains(sourceSeries.Relations, r => r.RelationKind == RelationKind.Annual && r.TargetSeriesId == existingRelationshipSeries.Id); - Assert.Contains(sourceSeries.Relations, r => r.RelationKind == RelationKind.SideStory && r.TargetSeriesId == series2.Id); - } - - - - // Sequel/Prequel - [Fact] - public async Task Relationships_Sequel_CreatesPrequel() - { - await ResetDb(); - - const string seriesName = "Test - Relationships Source"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - var series2 = new SeriesBuilder("Test - Relationships Target") - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Relations = [new SeriesRelationship() - { - Relation = RelationKind.Sequel, - SeriesName = new ALMediaTitle() - { - PreferredTitle = series2.Name, - EnglishTitle = null, - NativeTitle = series2.Name, - RomajiTitle = series2.Name, - }, - PlusMediaFormat = PlusMediaFormat.Manga - }] - }, 1); - - // Repull Series and validate what is overwritten - var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sourceSeries); - Assert.Single(sourceSeries.Relations); - Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); - - var sequel = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(sequel); - Assert.Equal(seriesName, sequel.Relations.First().TargetSeries.Name); - } - - [Fact] - public async Task Relationships_Prequel_CreatesSequel() - { - await ResetDb(); - - // ID 1: Blue Lock - Episode Nagi - var series = new SeriesBuilder("Blue Lock - Episode Nagi") - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - // ID 2: Blue Lock - var series2 = new SeriesBuilder("Blue Lock") - .WithLibraryId(1) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series2); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableRelationships = true; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - // Apply to Blue Lock - Episode Nagi (ID 1), setting Blue Lock (ID 2) as its prequel - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = "Blue Lock - Episode Nagi", // The series we're updating metadata for - Relations = [new SeriesRelationship() - { - Relation = RelationKind.Prequel, // Blue Lock is the prequel to Nagi - SeriesName = new ALMediaTitle() - { - PreferredTitle = "Blue Lock", - EnglishTitle = "Blue Lock", - NativeTitle = "ブルーロック", - RomajiTitle = "Blue Lock", - }, - PlusMediaFormat = PlusMediaFormat.Manga, - AniListId = 106130, - MalId = 114745, - Provider = ScrobbleProvider.AniList - }] - }, 1); // Apply to series ID 1 (Nagi) - - // Verify Blue Lock - Episode Nagi has Blue Lock as prequel - var nagiSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(nagiSeries); - Assert.Single(nagiSeries.Relations); - Assert.Equal("Blue Lock", nagiSeries.Relations.First().TargetSeries.Name); - Assert.Equal(RelationKind.Prequel, nagiSeries.Relations.First().RelationKind); - - // Verify Blue Lock has Blue Lock - Episode Nagi as sequel - var blueLockSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); - Assert.NotNull(blueLockSeries); - Assert.Single(blueLockSeries.Relations); - Assert.Equal("Blue Lock - Episode Nagi", blueLockSeries.Relations.First().TargetSeries.Name); - Assert.Equal(RelationKind.Sequel, blueLockSeries.Relations.First().RelationKind); - } - - - #endregion - - #region Blacklist - - [Fact] - public async Task Blacklist_Genres() - { - await ResetDb(); - - const string seriesName = "Test - Blacklist Genres"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.EnableGenres = true; - metadataSettings.Blacklist = ["Sports", "Action"]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Boxing", "Sports", "Action"], - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[] {"Boxing"}.OrderBy(s => s), postSeries.Metadata.Genres.Select(t => t.Title).OrderBy(s => s)); - } - - - [Fact] - public async Task Blacklist_Tags() - { - await ResetDb(); - - const string seriesName = "Test - Blacklist Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.EnableGenres = true; - metadataSettings.Blacklist = ["Sports", "Action"]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}, new MetadataTagDto() {Name = "Sports"}, new MetadataTagDto() {Name = "Action"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[] {"Boxing"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); - } - - // Blacklist Tag - - // Field Map then Blacklist Genre - - // Field Map then Blacklist Tag - - #endregion - - #region Whitelist - - [Fact] - public async Task Whitelist_Tags() - { - await ResetDb(); - - const string seriesName = "Test - Whitelist Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.Whitelist = ["Sports", "Action"]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}, new MetadataTagDto() {Name = "Sports"}, new MetadataTagDto() {Name = "Action"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[] {"Sports", "Action"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); - } - - [Fact] - public async Task Whitelist_WithFieldMap_Tags() - { - await ResetDb(); - - const string seriesName = "Test - Whitelist Tags"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Tag, - SourceValue = "Boxing", - DestinationType = MetadataFieldType.Tag, - DestinationValue = "Sports", - ExcludeFromSource = false - - }]; - metadataSettings.Whitelist = ["Sports", "Action"]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Boxing"}, new MetadataTagDto() {Name = "Action"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(new[] {"Sports", "Action"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); - } - - #endregion - - #region Field Mapping - - [Fact] - public async Task FieldMap_GenreToGenre_KeepSource_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - metadataSettings.Overrides = [MetadataSettingField.Genres]; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Genre, - SourceValue = "Ecchi", - DestinationType = MetadataFieldType.Genre, - DestinationValue = "Fanservice", - ExcludeFromSource = false - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal( - new[] { "Ecchi", "Fanservice" }.OrderBy(s => s), - postSeries.Metadata.Genres.Select(g => g.Title).OrderBy(s => s) - ); - } - - [Fact] - public async Task FieldMap_GenreToGenre_RemoveSource_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - metadataSettings.Overrides = [MetadataSettingField.Genres]; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Genre, - SourceValue = "Ecchi", - DestinationType = MetadataFieldType.Genre, - DestinationValue = "Fanservice", - ExcludeFromSource = true - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Fanservice"], postSeries.Metadata.Genres.Select(g => g.Title)); - } - - [Fact] - public async Task FieldMap_TagToTag_KeepSource_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Tag Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Tag, - SourceValue = "Ecchi", - DestinationType = MetadataFieldType.Tag, - DestinationValue = "Fanservice", - ExcludeFromSource = false - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Ecchi"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal( - new[] { "Ecchi", "Fanservice" }.OrderBy(s => s), - postSeries.Metadata.Tags.Select(g => g.Title).OrderBy(s => s) - ); - } - - [Fact] - public async Task Tags_Existing_FieldMap_RemoveSource_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Tag Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableTags = true; - metadataSettings.Overrides = [MetadataSettingField.Genres]; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Tag, - SourceValue = "Ecchi", - DestinationType = MetadataFieldType.Tag, - DestinationValue = "Fanservice", - ExcludeFromSource = true - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Tags = [new MetadataTagDto() {Name = "Ecchi"}] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal(["Fanservice"], postSeries.Metadata.Tags.Select(g => g.Title)); - } - - [Fact] - public async Task FieldMap_GenreToTag_KeepSource_Modification() - { - await ResetDb(); - - const string seriesName = "Test - Genres Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - metadataSettings.EnableTags = true; - metadataSettings.Overrides = [MetadataSettingField.Genres, MetadataSettingField.Tags]; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Genre, - SourceValue = "Ecchi", - DestinationType = MetadataFieldType.Tag, - DestinationValue = "Fanservice", - ExcludeFromSource = false - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Genres = ["Ecchi"] - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal( - new[] {"Ecchi"}.OrderBy(s => s), - postSeries.Metadata.Genres.Select(g => g.Title).OrderBy(s => s) - ); - Assert.Equal( - new[] {"Fanservice"}.OrderBy(s => s), - postSeries.Metadata.Tags.Select(g => g.Title).OrderBy(s => s) - ); - } - - - - [Fact] - public async Task FieldMap_GenreToGenre_RemoveSource_NoExternalGenre_NoModification() - { - await ResetDb(); - - const string seriesName = "Test - Genres Field Mapping"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithGenre(_genreLookup["Action"]) - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnableGenres = true; - metadataSettings.EnableTags = true; - metadataSettings.Overrides = [MetadataSettingField.Genres, MetadataSettingField.Tags]; - metadataSettings.FieldMappings = [new MetadataFieldMapping() - { - SourceType = MetadataFieldType.Genre, - SourceValue = "Action", - DestinationType = MetadataFieldType.Genre, - DestinationValue = "Adventure", - ExcludeFromSource = true - - }]; - - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - }, 1); - - // Repull Series and validate what is overwritten - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - Assert.Equal( - new[] {"Action"}.OrderBy(s => s), - postSeries.Metadata.Genres.Select(g => g.Title).OrderBy(s => s) - ); - } - - #endregion - - - - protected override async Task ResetDb() - { - Context.Series.RemoveRange(Context.Series); - Context.AppUser.RemoveRange(Context.AppUser); - Context.Genre.RemoveRange(Context.Genre); - Context.Tag.RemoveRange(Context.Tag); - Context.Person.RemoveRange(Context.Person); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = false; - metadataSettings.EnableSummary = false; - metadataSettings.EnableCoverImage = false; - metadataSettings.EnableLocalizedName = false; - metadataSettings.EnableGenres = false; - metadataSettings.EnablePeople = false; - metadataSettings.EnableRelationships = false; - metadataSettings.EnableTags = false; - metadataSettings.EnablePublicationStatus = false; - metadataSettings.EnableStartDate = false; - metadataSettings.FieldMappings = []; - metadataSettings.AgeRatingMappings = new Dictionary(); - Context.MetadataSettings.Update(metadataSettings); - - await Context.SaveChangesAsync(); - - Context.AppUser.Add(new AppUserBuilder("Joe", "Joe") - .WithRole(PolicyConstants.AdminRole) - .WithLibrary(await Context.Library.FirstAsync(l => l.Id == 1)) - .Build()); - - // Create a bunch of Genres for this test and store their string in _genreLookup - _genreLookup.Clear(); - var g1 = new GenreBuilder("Action").Build(); - var g2 = new GenreBuilder("Ecchi").Build(); - Context.Genre.Add(g1); - Context.Genre.Add(g2); - _genreLookup.Add("Action", g1); - _genreLookup.Add("Ecchi", g2); - - _tagLookup.Clear(); - var t1 = new TagBuilder("H").Build(); - var t2 = new TagBuilder("Boxing").Build(); - Context.Tag.Add(t1); - Context.Tag.Add(t2); - _tagLookup.Add("H", t1); - _tagLookup.Add("Boxing", t2); - - _personLookup.Clear(); - var p1 = new PersonBuilder("Johnny Twowheeler").Build(); - var p2 = new PersonBuilder("Boxing").Build(); - Context.Person.Add(p1); - Context.Person.Add(p2); - _personLookup.Add("Johnny Twowheeler", p1); - _personLookup.Add("Batman Robin", p2); - - await Context.SaveChangesAsync(); - } - - private static SeriesStaffDto CreateStaff(string first, string last, string role) - { - return new SeriesStaffDto() {Name = $"{first} {last}", Role = role, Url = "", FirstName = first, LastName = last}; - } - - private static SeriesCharacter CreateCharacter(string first, string last, CharacterRole role) - { - return new SeriesCharacter() {Name = $"{first} {last}", Description = "", Url = "", ImageUrl = "", Role = role}; - } -} diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs deleted file mode 100644 index f2c87e1ad..000000000 --- a/API.Tests/Services/ImageServiceTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System.IO; -using System.Linq; -using System.Text; -using API.Entities.Enums; -using API.Services; -using NetVips; -using Xunit; -using Image = NetVips.Image; - -namespace API.Tests.Services; - -public class ImageServiceTests -{ - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/Covers"); - private readonly string _testDirectoryColorScapes = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/ColorScapes"); - private const string OutputPattern = "_output"; - private const string BaselinePattern = "_baseline"; - - /// - /// Run this once to get the baseline generation - /// - [Fact] - public void GenerateBaseline() - { - GenerateFiles(BaselinePattern); - Assert.True(true); - } - - /// - /// Change the Scaling/Crop code then run this continuously - /// - [Fact] - public void TestScaling() - { - GenerateFiles(OutputPattern); - GenerateHtmlFile(); - Assert.True(true); - } - - private void GenerateFiles(string outputExtension) - { - // Step 1: Delete any images that have _output in the name - var outputFiles = Directory.GetFiles(_testDirectory, "*_output.*"); - foreach (var file in outputFiles) - { - File.Delete(file); - } - - // Step 2: Scan the _testDirectory for images - var imageFiles = Directory.GetFiles(_testDirectory, "*.*") - .Where(file => !file.EndsWith("html")) - .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) - .ToList(); - - // Step 3: Process each image - foreach (var imagePath in imageFiles) - { - var fileName = Path.GetFileNameWithoutExtension(imagePath); - var dims = CoverImageSize.Default.GetDimensions(); - using var sourceImage = Image.NewFromFile(imagePath, false, Enums.Access.SequentialUnbuffered); - - var size = ImageService.GetSizeForDimensions(sourceImage, dims.Width, dims.Height); - var crop = ImageService.GetCropForDimensions(sourceImage, dims.Width, dims.Height); - - using var thumbnail = Image.Thumbnail(imagePath, dims.Width, dims.Height, - size: size, - crop: crop); - - var outputFileName = fileName + outputExtension + ".png"; - thumbnail.WriteToFile(Path.Join(_testDirectory, outputFileName)); - } - } - - private void GenerateHtmlFile() - { - var imageFiles = Directory.GetFiles(_testDirectory, "*.*") - .Where(file => !file.EndsWith("html")) - .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) - .ToList(); - - var htmlBuilder = new StringBuilder(); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("Image Comparison"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("
"); - - foreach (var imagePath in imageFiles) - { - var fileName = Path.GetFileNameWithoutExtension(imagePath); - var baselinePath = Path.Combine(_testDirectory, fileName + "_baseline.png"); - var outputPath = Path.Combine(_testDirectory, fileName + "_output.png"); - var dims = CoverImageSize.Default.GetDimensions(); - - using var sourceImage = Image.NewFromFile(imagePath, false, Enums.Access.SequentialUnbuffered); - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine($"

{fileName} ({((double) sourceImage.Width / sourceImage.Height).ToString("F2")}) - {ImageService.WillScaleWell(sourceImage, dims.Width, dims.Height)}

"); - htmlBuilder.AppendLine($"\"{fileName}\""); - if (File.Exists(baselinePath)) - { - htmlBuilder.AppendLine($"\"{fileName}"); - } - if (File.Exists(outputPath)) - { - htmlBuilder.AppendLine($"\"{fileName}"); - } - htmlBuilder.AppendLine("
"); - } - - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - - File.WriteAllText(Path.Combine(_testDirectory, "index.html"), htmlBuilder.ToString()); - } - - - [Fact] - public void TestColorScapes() - { - // Step 1: Delete any images that have _output in the name - var outputFiles = Directory.GetFiles(_testDirectoryColorScapes, "*_output.*"); - foreach (var file in outputFiles) - { - File.Delete(file); - } - - // Step 2: Scan the _testDirectory for images - var imageFiles = Directory.GetFiles(_testDirectoryColorScapes, "*.*") - .Where(file => !file.EndsWith("html")) - .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) - .ToList(); - - // Step 3: Process each image - foreach (var imagePath in imageFiles) - { - var fileName = Path.GetFileNameWithoutExtension(imagePath); - var colors = ImageService.CalculateColorScape(imagePath); - - // Generate primary color image - GenerateColorImage(colors.Primary, Path.Combine(_testDirectoryColorScapes, $"{fileName}_primary_output.png")); - - // Generate secondary color image - GenerateColorImage(colors.Secondary, Path.Combine(_testDirectoryColorScapes, $"{fileName}_secondary_output.png")); - } - - // Step 4: Generate HTML file - GenerateHtmlFileForColorScape(); - Assert.True(true); - } - - private static void GenerateColorImage(string hexColor, string outputPath) - { - var (r, g, b) = ImageService.HexToRgb(hexColor); - using var blackImage = Image.Black(200, 100); - using var colorImage = blackImage.NewFromImage(r, g, b); - colorImage.WriteToFile(outputPath); - } - - private void GenerateHtmlFileForColorScape() - { - var imageFiles = Directory.GetFiles(_testDirectoryColorScapes, "*.*") - .Where(file => !file.EndsWith("html")) - .Where(file => !file.Contains(OutputPattern) && !file.Contains(BaselinePattern)) - .ToList(); - - var htmlBuilder = new StringBuilder(); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("Color Scape Comparison"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("
"); - - foreach (var imagePath in imageFiles) - { - var fileName = Path.GetFileNameWithoutExtension(imagePath); - var primaryPath = Path.Combine(_testDirectoryColorScapes, $"{fileName}_primary_output.png"); - var secondaryPath = Path.Combine(_testDirectoryColorScapes, $"{fileName}_secondary_output.png"); - - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine($"

{fileName}

"); - htmlBuilder.AppendLine($"\"{fileName}\""); - if (File.Exists(primaryPath)) - { - htmlBuilder.AppendLine($"\"{fileName}"); - } - if (File.Exists(secondaryPath)) - { - htmlBuilder.AppendLine($"\"{fileName}"); - } - htmlBuilder.AppendLine("
"); - } - - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - - File.WriteAllText(Path.Combine(_testDirectoryColorScapes, "colorscape_index.html"), htmlBuilder.ToString()); - } -} diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index a732b2526..32ad8f645 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -1,41 +1,36 @@ using System; using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; +using API.Data; using API.Data.Metadata; -using API.Data.Repositories; +using API.Entities; using API.Entities.Enums; +using API.Extensions; +using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; using API.SignalR; -using API.Tests.Helpers; -using Hangfire; +using AutoMapper; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Services; -public class MockReadingItemService : IReadingItemService +internal class MockReadingItemService : IReadingItemService { - private readonly BasicParser _basicParser; - private readonly ComicVineParser _comicVineParser; - private readonly ImageParser _imageParser; - private readonly BookParser _bookParser; - private readonly PdfParser _pdfParser; + private readonly IDefaultParser _defaultParser; - public MockReadingItemService(IDirectoryService directoryService, IBookService bookService) + public MockReadingItemService(IDefaultParser defaultParser) { - _imageParser = new ImageParser(directoryService); - _basicParser = new BasicParser(directoryService, _imageParser); - _bookParser = new BookParser(directoryService, bookService, _basicParser); - _comicVineParser = new ComicVineParser(directoryService); - _pdfParser = new PdfParser(directoryService); + _defaultParser = defaultParser; } public ComicInfo GetComicInfo(string filePath) @@ -58,57 +53,99 @@ public class MockReadingItemService : IReadingItemService throw new NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo Parse(string path, string rootPath, LibraryType type) { - if (_comicVineParser.IsApplicable(path, type)) - { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_imageParser.IsApplicable(path, type)) - { - return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_bookParser.IsApplicable(path, type)) - { - return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_pdfParser.IsApplicable(path, type)) - { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_basicParser.IsApplicable(path, type)) - { - return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - - return null; + return _defaultParser.Parse(path, rootPath, type); } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo ParseFile(string path, string rootPath, LibraryType type) { - return Parse(path, rootPath, libraryRoot, type, enableMetadata); + return _defaultParser.Parse(path, rootPath, type); } } -public class ParseScannedFilesTests : AbstractDbTest +public class ParseScannedFilesTests { private readonly ILogger _logger = Substitute.For>(); - private readonly ScannerHelper _scannerHelper; + private readonly IUnitOfWork _unitOfWork; - public ParseScannedFilesTests(ITestOutputHelper testOutputHelper) + private readonly DbConnection _connection; + private readonly DataContext _context; + + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string DataDirectory = "C:/data/"; + + public ParseScannedFilesTests() { + var contextOptions = new DbContextOptionsBuilder() + .UseSqlite(CreateInMemoryDatabase()) + .Options; + _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; + + _context = new DataContext(contextOptions); + Task.Run(SeedDb).GetAwaiter().GetResult(); + + _unitOfWork = new UnitOfWork(_context, Substitute.For(), null); + // Since ProcessFile relies on _readingItemService, we can implement our own versions of _readingItemService so we have control over how the calls work - GlobalConfiguration.Configuration.UseInMemoryStorage(); - _scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper); } - protected override async Task ResetDb() + #region Setup + + private static DbConnection CreateInMemoryDatabase() { - Context.Series.RemoveRange(Context.Series.ToList()); + var connection = new SqliteConnection("Filename=:memory:"); - await Context.SaveChangesAsync(); + connection.Open(); + + return connection; } + private async Task SeedDb() + { + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); + + await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); + + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; + + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; + + _context.ServerSetting.Update(setting); + + _context.Library.Add(new LibraryBuilder("Manga") + .WithFolderPath(new FolderPathBuilder(DataDirectory).Build()) + .Build()); + return await _context.SaveChangesAsync() > 0; + } + + private async Task ResetDB() + { + _context.Series.RemoveRange(_context.Series.ToList()); + + await _context.SaveChangesAsync(); + } + + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(DataDirectory); + + return fileSystem; + } + + #endregion + #region MergeName // NOTE: I don't think I can test MergeName as it relies on Tracking Files, which is more complicated than I need @@ -181,45 +218,46 @@ public class ParseScannedFilesTests : AbstractDbTest #region ScanLibrariesForSeries - /// - /// Test that when a folder has 2 series with a localizedSeries, they combine into one final series - /// - // [Fact] - // public async Task ScanLibrariesForSeries_ShouldCombineSeries() - // { - // // TODO: Implement these unit tests - // } - [Fact] public async Task ScanLibrariesForSeries_ShouldFindFiles() { var fileSystem = new MockFileSystem(); - fileSystem.AddDirectory(Root + "Data/"); - fileSystem.AddFile(Root + "Data/Accel World v1.cbz", new MockFileData(string.Empty)); - fileSystem.AddFile(Root + "Data/Accel World v2.cbz", new MockFileData(string.Empty)); - fileSystem.AddFile(Root + "Data/Accel World v2.pdf", new MockFileData(string.Empty)); - fileSystem.AddFile(Root + "Data/Nothing.pdf", new MockFileData(string.Empty)); + fileSystem.AddDirectory("C:/Data/"); + fileSystem.AddFile("C:/Data/Accel World v1.cbz", new MockFileData(string.Empty)); + fileSystem.AddFile("C:/Data/Accel World v2.cbz", new MockFileData(string.Empty)); + fileSystem.AddFile("C:/Data/Accel World v2.pdf", new MockFileData(string.Empty)); + fileSystem.AddFile("C:/Data/Nothing.pdf", new MockFileData(string.Empty)); var ds = new DirectoryService(Substitute.For>(), fileSystem); var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); + new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); + + var parsedSeries = new Dictionary>(); + + Task TrackFiles(Tuple> parsedInfo) + { + var skippedScan = parsedInfo.Item1; + var parsedFiles = parsedInfo.Item2; + if (parsedFiles.Count == 0) return Task.CompletedTask; + + var foundParsedSeries = new ParsedSeries() + { + Name = parsedFiles.First().Series, + NormalizedName = parsedFiles.First().Series.ToNormalized(), + Format = parsedFiles.First().Format + }; + + parsedSeries.Add(foundParsedSeries, parsedFiles); + return Task.CompletedTask; + } - var library = - await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, - LibraryIncludes.Folders | LibraryIncludes.FileTypes); - Assert.NotNull(library); - - library.Type = LibraryType.Manga; - var parsedSeries = await psf.ScanLibrariesForSeries(library, new List() {Root + "Data/"}, false, - await UnitOfWork.SeriesRepository.GetFolderPathMap(1)); + await psf.ScanLibrariesForSeries(LibraryType.Manga, + new List() {"C:/Data/"}, "libraryName", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles); - // Assert.Equal(3, parsedSeries.Values.Count); - // Assert.NotEmpty(parsedSeries.Keys.Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World"))); - - Assert.Equal(3, parsedSeries.Count); - Assert.NotEmpty(parsedSeries.Select(p => p.ParsedSeries).Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World"))); + Assert.Equal(3, parsedSeries.Values.Count); + Assert.NotEmpty(parsedSeries.Keys.Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World"))); } #endregion @@ -248,16 +286,15 @@ public class ParseScannedFilesTests : AbstractDbTest var fileSystem = CreateTestFilesystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); + new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); var directoriesSeen = new HashSet(); - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, - LibraryIncludes.Folders | LibraryIncludes.FileTypes); - var scanResults = await psf.ScanFiles("C:/Data/", true, await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); - foreach (var scanResult in scanResults) + await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), + (files, directoryPath) => { - directoriesSeen.Add(scanResult.Folder); - } + directoriesSeen.Add(directoryPath); + return Task.CompletedTask; + }); Assert.Equal(2, directoriesSeen.Count); } @@ -268,20 +305,14 @@ public class ParseScannedFilesTests : AbstractDbTest var fileSystem = CreateTestFilesystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, - LibraryIncludes.Folders | LibraryIncludes.FileTypes); - Assert.NotNull(library); + new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); var directoriesSeen = new HashSet(); - var scanResults = await psf.ScanFiles("C:/Data/", false, - await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); - - foreach (var scanResult in scanResults) + await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, directoryPath) => { - directoriesSeen.Add(scanResult.Folder); - } + directoriesSeen.Add(directoryPath); + return Task.CompletedTask; + }); Assert.Single(directoriesSeen); directoriesSeen.TryGetValue("C:/Data/", out var actual); @@ -303,14 +334,17 @@ public class ParseScannedFilesTests : AbstractDbTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); + new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, - LibraryIncludes.Folders | LibraryIncludes.FileTypes); - Assert.NotNull(library); - var scanResults = await psf.ScanFiles("C:/Data", true, await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); + var callCount = 0; + await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) => + { + callCount++; - Assert.Equal(2, scanResults.Count); + return Task.CompletedTask; + }); + + Assert.Equal(2, callCount); } @@ -332,235 +366,17 @@ public class ParseScannedFilesTests : AbstractDbTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); + new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, - LibraryIncludes.Folders | LibraryIncludes.FileTypes); - Assert.NotNull(library); - var scanResults = await psf.ScanFiles("C:/Data", false, - await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); + var callCount = 0; + await psf.ProcessFiles("C:/Data", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) => + { + callCount++; + return Task.CompletedTask; + }); - Assert.Single(scanResults); + Assert.Equal(1, callCount); } - - - #endregion - - // TODO: Add back in (removed for Hotfix v0.8.5.x) - //[Fact] - public async Task HasSeriesFolderNotChangedSinceLastScan_AllSeriesFoldersHaveChanges() - { - const string testcase = "Subfolders always scanning all series changes - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var scanner = _scannerHelper.CreateServices(ds, fs); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Equal(4, postLib.Series.Count); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(2, spiceAndWolf.Volumes.Count); - - var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); - Assert.Single(frieren.Volumes); - - var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life"); - Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count); - - await Task.Delay(1100); // Ensure at least one second has passed since library scan - - // Add a new chapter to a volume of the series, and scan. Validate that only, and all directories of this - // series are marked as HasChanged - var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "The Executioner and Her Way of Life"), - "The Executioner and Her Way of Life Vol. 1"); - File.Copy(Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0001.cbz"), - Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0002.cbz")); - - // 4 series, of which 2 have volumes as directories - var folderMap = await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id); - Assert.Equal(6, folderMap.Count); - - var res = await psf.ScanFiles(testDirectoryPath, true, folderMap, postLib); - var changes = res.Where(sc => sc.HasChanged).ToList(); - Assert.Equal(2, changes.Count); - // Only volumes of The Executioner and Her Way of Life should be marked as HasChanged (Spice and Wolf also has 2 volumes dirs) - Assert.Equal(2, changes.Count(sc => sc.Folder.Contains("The Executioner and Her Way of Life"))); - } - - [Fact] - public async Task HasSeriesFolderNotChangedSinceLastScan_PublisherLayout() - { - const string testcase = "Subfolder always scanning fix publisher layout - Comic.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var scanner = _scannerHelper.CreateServices(ds, fs); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Equal(4, postLib.Series.Count); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(2, spiceAndWolf.Volumes.Count); - - var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); - Assert.Equal(2, frieren.Volumes.Count); - - await Task.Delay(1100); // Ensure at least one second has passed since library scan - - // Add a volume to a series, and scan. Ensure only this series is marked as HasChanged - var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "YenPress"), "The Executioner and Her Way of Life"); - File.Copy(Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1.cbz"), - Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 2.cbz")); - - var res = await psf.ScanFiles(testDirectoryPath, true, - await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); - var changes = res.Count(sc => sc.HasChanged); - Assert.Equal(1, changes); - } - - // TODO: Add back in (removed for Hotfix v0.8.5.x) - //[Fact] - public async Task SubFoldersNoSubFolders_SkipAll() - { - const string testcase = "Subfolders and files at root - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var scanner = _scannerHelper.CreateServices(ds, fs); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(3, spiceAndWolf.Volumes.Count); - Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - // Needs to be actual time as the write time is now, so if we set LastFolderChecked in the past - // it'll always a scan as it was changed since the last scan. - await Task.Delay(1100); // Ensure at least one second has passed since library scan - - var res = await psf.ScanFiles(testDirectoryPath, true, - await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); - Assert.DoesNotContain(res, sc => sc.HasChanged); - } - - [Fact] - public async Task SubFoldersNoSubFolders_ScanAllAfterAddInRoot() - { - const string testcase = "Subfolders and files at root - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var scanner = _scannerHelper.CreateServices(ds, fs); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(3, spiceAndWolf.Volumes.Count); - Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - spiceAndWolf.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(2)); - Context.Series.Update(spiceAndWolf); - await Context.SaveChangesAsync(); - - // Add file at series root - var spiceAndWolfDir = Path.Join(testDirectoryPath, "Spice and Wolf"); - File.Copy(Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 1.cbz"), - Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 4.cbz")); - - var res = await psf.ScanFiles(testDirectoryPath, true, - await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); - var changes = res.Count(sc => sc.HasChanged); - Assert.Equal(2, changes); - } - - [Fact] - public async Task SubFoldersNoSubFolders_ScanAllAfterAddInSubFolder() - { - const string testcase = "Subfolders and files at root - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var fs = new FileSystem(); - var ds = new DirectoryService(Substitute.For>(), fs); - var psf = new ParseScannedFiles(Substitute.For>(), ds, - new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - - var scanner = _scannerHelper.CreateServices(ds, fs); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(3, spiceAndWolf.Volumes.Count); - Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - spiceAndWolf.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(2)); - Context.Series.Update(spiceAndWolf); - await Context.SaveChangesAsync(); - - // Add file in subfolder - var spiceAndWolfDir = Path.Join(Path.Join(testDirectoryPath, "Spice and Wolf"), "Spice and Wolf Vol. 3"); - File.Copy(Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3 Ch. 0011.cbz"), - Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3 Ch. 0013.cbz")); - - var res = await psf.ScanFiles(testDirectoryPath, true, - await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); - var changes = res.Count(sc => sc.HasChanged); - Assert.Equal(2, changes); - } } diff --git a/API.Tests/Services/PersonServiceTests.cs b/API.Tests/Services/PersonServiceTests.cs deleted file mode 100644 index 5c1929b1c..000000000 --- a/API.Tests/Services/PersonServiceTests.cs +++ /dev/null @@ -1,286 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using Xunit; - -namespace API.Tests.Services; - -public class PersonServiceTests: AbstractDbTest -{ - - [Fact] - public async Task PersonMerge_KeepNonEmptyMetadata() - { - var ps = new PersonService(UnitOfWork); - - var person1 = new Person - { - Name = "Casey Delores", - NormalizedName = "Casey Delores".ToNormalized(), - HardcoverId = "ANonEmptyId", - MalId = 12, - }; - - var person2 = new Person - { - Name= "Delores Casey", - NormalizedName = "Delores Casey".ToNormalized(), - Description = "Hi, I'm Delores Casey!", - Aliases = [new PersonAliasBuilder("Casey, Delores").Build()], - AniListId = 27, - }; - - UnitOfWork.PersonRepository.Attach(person1); - UnitOfWork.PersonRepository.Attach(person2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person1); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - var person = allPeople[0]; - Assert.Equal("Casey Delores", person.Name); - Assert.NotEmpty(person.Description); - Assert.Equal(27, person.AniListId); - Assert.NotNull(person.HardcoverId); - Assert.NotEmpty(person.HardcoverId); - Assert.Contains(person.Aliases, pa => pa.Alias == "Delores Casey"); - Assert.Contains(person.Aliases, pa => pa.Alias == "Casey, Delores"); - } - - [Fact] - public async Task PersonMerge_MergedPersonDestruction() - { - var ps = new PersonService(UnitOfWork); - - var person1 = new Person - { - Name = "Casey Delores", - NormalizedName = "Casey Delores".ToNormalized(), - }; - - var person2 = new Person - { - Name = "Delores Casey", - NormalizedName = "Delores Casey".ToNormalized(), - }; - - UnitOfWork.PersonRepository.Attach(person1); - UnitOfWork.PersonRepository.Attach(person2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person1); - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - } - - [Fact] - public async Task PersonMerge_RetentionChapters() - { - var ps = new PersonService(UnitOfWork); - - var library = new LibraryBuilder("My Library").Build(); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var user = new AppUserBuilder("Amelia", "amelia@localhost") - .WithLibrary(library).Build(); - UnitOfWork.UserRepository.Add(user); - - var person = new PersonBuilder("Jillian Cowan").Build(); - - var person2 = new PersonBuilder("Cowan Jillian").Build(); - - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .Build(); - - var chapter2 = new ChapterBuilder("2") - .WithPerson(person2, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - var series2 = new SeriesBuilder("Test 2") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("2") - .WithChapter(chapter2) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - UnitOfWork.SeriesRepository.Add(series2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - var mergedPerson = allPeople[0]; - - Assert.Equal("Jillian Cowan", mergedPerson.Name); - - var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(1, 1, PersonRole.Editor); - Assert.Equal(2, chapters.Count()); - - chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(1, ChapterIncludes.People); - Assert.NotNull(chapter); - Assert.Single(chapter.People); - - chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2, ChapterIncludes.People); - Assert.NotNull(chapter2); - Assert.Single(chapter2.People); - - Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId); - } - - [Fact] - public async Task PersonMerge_NoDuplicateChaptersOrSeries() - { - await ResetDb(); - - var ps = new PersonService(UnitOfWork); - - var library = new LibraryBuilder("My Library").Build(); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var user = new AppUserBuilder("Amelia", "amelia@localhost") - .WithLibrary(library).Build(); - UnitOfWork.UserRepository.Add(user); - - var person = new PersonBuilder("Jillian Cowan").Build(); - - var person2 = new PersonBuilder("Cowan Jillian").Build(); - - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Colorist) - .Build(); - - var chapter2 = new ChapterBuilder("2") - .WithPerson(person2, PersonRole.Editor) - .WithPerson(person, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Editor) - .Build()) - .Build(); - - var series2 = new SeriesBuilder("Test 2") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("2") - .WithChapter(chapter2) - .Build()) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Colorist) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - UnitOfWork.SeriesRepository.Add(series2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person); - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - var mergedPerson = await UnitOfWork.PersonRepository.GetPersonById(person.Id, PersonIncludes.All); - Assert.NotNull(mergedPerson); - Assert.Equal(3, mergedPerson.ChapterPeople.Count); - Assert.Equal(3, mergedPerson.SeriesMetadataPeople.Count); - - chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter.Id, ChapterIncludes.People); - Assert.NotNull(chapter); - Assert.Equal(2, chapter.People.Count); - Assert.Single(chapter.People.Select(p => p.Person.Id).Distinct()); - Assert.Contains(chapter.People, p => p.Role == PersonRole.Editor); - Assert.Contains(chapter.People, p => p.Role == PersonRole.Colorist); - - chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter2.Id, ChapterIncludes.People); - Assert.NotNull(chapter2); - Assert.Single(chapter2.People); - Assert.Contains(chapter2.People, p => p.Role == PersonRole.Editor); - Assert.DoesNotContain(chapter2.People, p => p.Role == PersonRole.Colorist); - - series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Metadata); - Assert.NotNull(series); - Assert.Single(series.Metadata.People); - Assert.Contains(series.Metadata.People, p => p.Role == PersonRole.Editor); - Assert.DoesNotContain(series.Metadata.People, p => p.Role == PersonRole.Colorist); - - series2 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series2.Id, SeriesIncludes.Metadata); - Assert.NotNull(series2); - Assert.Equal(2, series2.Metadata.People.Count); - Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Editor); - Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Colorist); - - - } - - [Fact] - public async Task PersonAddAlias_NoOverlap() - { - await ResetDb(); - - UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jillian Cowan").Build()); - UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jilly Cowan").WithAlias("Jolly Cowan").Build()); - await UnitOfWork.CommitAsync(); - - var ps = new PersonService(UnitOfWork); - - var person1 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jillian Cowan"); - var person2 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jilly Cowan"); - Assert.NotNull(person1); - Assert.NotNull(person2); - - // Overlap on Name - var success = await ps.UpdatePersonAliasesAsync(person1, ["Jilly Cowan"]); - Assert.False(success); - - // Overlap on alias - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan"]); - Assert.False(success); - - // No overlap - success = await ps.UpdatePersonAliasesAsync(person2, ["Jilly Joy Cowan"]); - Assert.True(success); - - // Some overlap - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); - Assert.False(success); - - // Some overlap - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); - Assert.False(success); - - Assert.Single(person2.Aliases); - } - - protected override async Task ResetDb() - { - Context.Person.RemoveRange(Context.Person.ToList()); - - await Context.SaveChangesAsync(); - } -} diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/API.Tests/Services/ProcessSeriesTests.cs index 119e1bc10..ef5c45007 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/API.Tests/Services/ProcessSeriesTests.cs @@ -1,8 +1,23 @@ -namespace API.Tests.Services; +using System.IO; +using API.Data; +using API.Data.Metadata; +using API.Entities; +using API.Entities.Enums; +using API.Helpers; +using API.Helpers.Builders; +using API.Services; +using API.Services.Tasks.Metadata; +using API.Services.Tasks.Scanner; +using API.SignalR; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace API.Tests.Services; public class ProcessSeriesTests { - // TODO: Implement + #region UpdateSeriesMetadata diff --git a/API.Tests/Services/RatingServiceTests.cs b/API.Tests/Services/RatingServiceTests.cs deleted file mode 100644 index 15f4541d7..000000000 --- a/API.Tests/Services/RatingServiceTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using Hangfire; -using Hangfire.InMemory; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class RatingServiceTests: AbstractDbTest -{ - private readonly RatingService _ratingService; - - public RatingServiceTests() - { - _ratingService = new RatingService(UnitOfWork, Substitute.For(), Substitute.For>()); - } - - [Fact] - public async Task UpdateRating_ShouldSetRating() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb") - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - - JobStorage.Current = new InMemoryStorage(); - var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto - { - SeriesId = 1, - UserRating = 3, - }); - - Assert.True(result); - - var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))! - .Ratings; - Assert.NotEmpty(ratings); - Assert.Equal(3, ratings.First().Rating); - } - - [Fact] - public async Task UpdateRating_ShouldUpdateExistingRating() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb") - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - - var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto - { - SeriesId = 1, - UserRating = 3, - }); - - Assert.True(result); - - JobStorage.Current = new InMemoryStorage(); - var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) - .Ratings; - Assert.NotEmpty(ratings); - Assert.Equal(3, ratings.First().Rating); - - // Update the DB again - - var result2 = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto - { - SeriesId = 1, - UserRating = 5, - }); - - Assert.True(result2); - - var ratings2 = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) - .Ratings; - Assert.NotEmpty(ratings2); - Assert.True(ratings2.Count == 1); - Assert.Equal(5, ratings2.First().Rating); - } - - [Fact] - public async Task UpdateRating_ShouldClampRatingAt5() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb") - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - await Context.SaveChangesAsync(); - - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - - var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto - { - SeriesId = 1, - UserRating = 10, - }); - - Assert.True(result); - - JobStorage.Current = new InMemoryStorage(); - var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", - AppUserIncludes.Ratings)!) - .Ratings; - Assert.NotEmpty(ratings); - Assert.Equal(5, ratings.First().Rating); - } - - [Fact] - public async Task UpdateRating_ShouldReturnFalseWhenSeriesDoesntExist() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - await Context.SaveChangesAsync(); - - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - - var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto - { - SeriesId = 2, - UserRating = 5, - }); - - Assert.False(result); - - var ratings = user.Ratings; - Assert.Empty(ratings); - } - protected override async Task ResetDb() - { - Context.Series.RemoveRange(Context.Series.ToList()); - Context.AppUserRating.RemoveRange(Context.AppUserRating.ToList()); - Context.Genre.RemoveRange(Context.Genre.ToList()); - Context.CollectionTag.RemoveRange(Context.CollectionTag.ToList()); - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); - - await Context.SaveChangesAsync(); - } -} diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 0e4ab2701..1200c3097 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,19 +1,29 @@ using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; +using API.Data; using API.Data.Repositories; -using API.DTOs.Progress; +using API.DTOs; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Extensions; +using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; +using API.Services.Tasks; using API.SignalR; +using API.Tests.Helpers; +using AutoMapper; using Hangfire; using Hangfire.InMemory; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -21,16 +31,30 @@ using Xunit.Abstractions; namespace API.Tests.Services; -public class ReaderServiceTests: AbstractDbTest +public class ReaderServiceTests { private readonly ITestOutputHelper _testOutputHelper; + private readonly IUnitOfWork _unitOfWork; + private readonly DataContext _context; private readonly ReaderService _readerService; + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string DataDirectory = "C:/data/"; + public ReaderServiceTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; + var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options; - _readerService = new ReaderService(UnitOfWork, Substitute.For>(), + _context = new DataContext(contextOptions); + Task.Run(SeedDb).GetAwaiter().GetResult(); + + var config = new MapperConfiguration(cfg => cfg.AddProfile()); + var mapper = config.CreateMapper(); + _unitOfWork = new UnitOfWork(_context, mapper, null); + _readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); @@ -38,12 +62,55 @@ public class ReaderServiceTests: AbstractDbTest #region Setup - - protected override async Task ResetDb() + private static DbConnection CreateInMemoryDatabase() { - Context.Series.RemoveRange(Context.Series.ToList()); + var connection = new SqliteConnection("Filename=:memory:"); - await Context.SaveChangesAsync(); + connection.Open(); + + return connection; + } + + private async Task SeedDb() + { + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); + + await Seed.SeedSettings(_context, + new DirectoryService(Substitute.For>(), filesystem)); + + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; + + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; + + _context.ServerSetting.Update(setting); + + _context.Library.Add(new LibraryBuilder("Manga") + .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) + .Build()); + return await _context.SaveChangesAsync() > 0; + } + + private async Task ResetDb() + { + _context.Series.RemoveRange(_context.Series.ToList()); + + await _context.SaveChangesAsync(); + } + + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(DataDirectory); + + return fileSystem; } #endregion @@ -68,8 +135,9 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) .Build()) @@ -77,10 +145,10 @@ public class ReaderServiceTests: AbstractDbTest series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); Assert.Equal(0, (await _readerService.CapPageToChapter(1, -1)).Item1); @@ -97,22 +165,23 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); JobStorage.Current = new InMemoryStorage(); @@ -126,7 +195,7 @@ public class ReaderServiceTests: AbstractDbTest }, 1); Assert.True(successful); - Assert.NotNull(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); + Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); } [Fact] @@ -135,22 +204,23 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); JobStorage.Current = new InMemoryStorage(); var successful = await _readerService.SaveReadingProgress(new ProgressDto() @@ -163,7 +233,7 @@ public class ReaderServiceTests: AbstractDbTest }, 1); Assert.True(successful); - Assert.NotNull(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); + Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); Assert.True(await _readerService.SaveReadingProgress(new ProgressDto() { @@ -174,9 +244,7 @@ public class ReaderServiceTests: AbstractDbTest BookScrollId = "/h1/" }, 1)); - var userProgress = await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1); - Assert.NotNull(userProgress); - Assert.Equal("/h1/", userProgress.BookScrollId); + Assert.Equal("/h1/", (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).BookScrollId); } @@ -191,35 +259,34 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithPages(2) .Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var volumes = await UnitOfWork.VolumeRepository.GetVolumes(1); - await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); - await Context.SaveChangesAsync(); + var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1); + await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); + await _context.SaveChangesAsync(); - var userProgress = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); - Assert.NotNull(userProgress); - Assert.Equal(2, userProgress.Progresses.Count); + Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); } #endregion @@ -231,38 +298,39 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder("0") .WithPages(2) .Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var volumes = (await UnitOfWork.VolumeRepository.GetVolumes(1)).ToList(); - await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); + var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); + await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); - await Context.SaveChangesAsync(); - Assert.Equal(2, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + await _context.SaveChangesAsync(); + Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); - await _readerService.MarkChaptersAsUnread(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); - await Context.SaveChangesAsync(); + await _readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); + await _context.SaveChangesAsync(); - var progresses = (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; + var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; Assert.Equal(0, progresses.Max(p => p.PagesRead)); Assert.Equal(2, progresses.Count); } @@ -279,72 +347,41 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") + .WithNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") + .WithNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("2", actualChapter.Range); } - [Fact] - public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges() - { - // V1 -> V2 - await ResetDb(); - - var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder("1-2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3-4") - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .Build(); - series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build(); - - Context.Series.Add(series); - - Context.AppUser.Add(new AppUser() - { - UserName = "majora2007" - }); - - await Context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); - Assert.Equal("3-4", actualChapter.Volume.Name); - Assert.Equal("1", actualChapter.Range); - } - [Fact] public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats() { @@ -372,20 +409,19 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("31", actualChapter.Range); } @@ -396,35 +432,37 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") + .WithNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") + .WithNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("21", actualChapter.Range); } @@ -435,36 +473,38 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") + .WithNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") + .WithNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("21", actualChapter.Range); } @@ -474,32 +514,33 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("21").Build()) - .WithChapter(new ChapterBuilder("22").Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) + .WithNumber(1) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); - Assert.Equal("21", actualChapter.Range); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + Assert.Equal("1", actualChapter.Range); } [Fact] @@ -508,38 +549,40 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("67").Build()) .Build()) .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithNumber(2) + .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); - Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + Assert.Equal("0", actualChapter.Range); } [Fact] @@ -549,24 +592,26 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); Assert.Equal(-1, nextChapter); @@ -579,19 +624,20 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); @@ -603,89 +649,58 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); } - // This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099) - [Fact] - public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume() - { - await ResetDb(); - - var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) - .Build()) - .Build(); - series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() - { - UserName = "majora2007" - }); - - await Context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); - Assert.Equal(-1, nextChapter); - } - - [Fact] - public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() - { - await ResetDb(); - - var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) - .Build()) - - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - .Build(); - series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() - { - UserName = "majora2007" - }); - - await Context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); - Assert.Equal(-1, nextChapter); - } + // This is commented out because, while valid, I can't solve how to make this pass + // [Fact] + // public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() + // { + // await ResetDb(); + // + // var series = new SeriesBuilder("Test") + // .WithVolume(new VolumeBuilder("0") + // .WithNumber(0) + // .WithChapter(new ChapterBuilder("1").Build()) + // .WithChapter(new ChapterBuilder("2").Build()) + // .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build()) + // .Build()) + // + // .WithVolume(new VolumeBuilder("1") + // .WithNumber(1) + // .WithChapter(new ChapterBuilder("2").Build()) + // .Build()) + // .Build(); + // series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + // + // _context.Series.Add(series); + // _context.AppUser.Add(new AppUser() + // { + // UserName = "majora2007" + // }); + // + // await _context.SaveChangesAsync(); + // + // var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); + // Assert.Equal(-1, nextChapter); + // } @@ -696,37 +711,32 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) - .WithChapter(new ChapterBuilder("B.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) - .Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -736,35 +746,28 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) - .Build()) - - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .WithPages(1) - .Build()) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -774,35 +777,29 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) - .Build()) - - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .WithPages(1) - .Build()) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 3, 4, 1); + var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1); Assert.Equal(-1, nextChapter); } @@ -814,36 +811,31 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) - .WithChapter(new ChapterBuilder("B.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) - .Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("B.cbz", actualChapter.Range); } @@ -854,31 +846,33 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") + .WithNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); var user = new AppUserBuilder("majora2007", "fake").Build(); - Context.AppUser.Add(user); + _context.AppUser.Add(user); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); await _readerService.MarkChaptersAsRead(user, 1, new List() { - series.Volumes[0].Chapters[0] + series.Volumes.First().Chapters.First() }); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(2, actualChapter.Volume.MinNumber); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + Assert.Equal(2, actualChapter.Volume.Number); } #endregion @@ -893,36 +887,38 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") + .WithNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") + .WithNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("1", actualChapter.Range); } @@ -934,34 +930,36 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") + .WithNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") + .WithNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("22", actualChapter.Range); } @@ -971,18 +969,11 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) .WithChapter(new ChapterBuilder("50").WithPages(1).Build()) .WithChapter(new ChapterBuilder("60").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .WithPages(1) - .Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithPages(1).WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") @@ -998,20 +989,20 @@ public class ReaderServiceTests: AbstractDbTest .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // prevChapter should be id from ch.21 from volume 2001 - var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 5, 7, 1); + var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 4, 7, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("21", actualChapter.Range); } @@ -1039,21 +1030,20 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("2", actualChapter.Range); } @@ -1064,33 +1054,34 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); Assert.Equal(2, prevChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("2", actualChapter.Range); } @@ -1101,20 +1092,21 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1130,19 +1122,20 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1157,27 +1150,32 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); + + + + var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1); Assert.Equal(-1, prevChapter); } @@ -1187,38 +1185,41 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("7").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("3").Build()) - .WithChapter(new ChapterBuilder("4").Build()) + .WithNumber(2) + .WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1); - var chapterInfoDto = await UnitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); + var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); Assert.Equal(1, chapterInfoDto.ChapterNumber.AsFloat()); // This is first chapter of first volume @@ -1232,21 +1233,22 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1262,39 +1264,34 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .Build()) - .WithChapter(new ChapterBuilder("B.cbz") - .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) - .Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1); Assert.NotEqual(-1, prevChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -1304,11 +1301,13 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -1316,19 +1315,21 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); + + + var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1); Assert.NotEqual(-1, prevChapter); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); - Assert.NotNull(actualChapter); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("22", actualChapter.Range); } @@ -1339,26 +1340,28 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") + .WithNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") + .WithNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); var user = new AppUserBuilder("majora2007", "fake").Build(); - Context.AppUser.Add(user); + _context.AppUser.Add(user); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); - var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(1, actualChapter.Volume.MinNumber); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + Assert.Equal(1, actualChapter.Volume.Number); } #endregion @@ -1370,7 +1373,7 @@ public class ReaderServiceTests: AbstractDbTest { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").Build()) .WithChapter(new ChapterBuilder("96").Build()) .Build()) @@ -1390,15 +1393,15 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1417,20 +1420,20 @@ public class ReaderServiceTests: AbstractDbTest .WithChapter(new ChapterBuilder("1").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithPages(4) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1456,20 +1459,20 @@ public class ReaderServiceTests: AbstractDbTest .WithChapter(new ChapterBuilder("1", "1-11").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithPages(4) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1507,14 +1510,14 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1542,7 +1545,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 2 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1557,21 +1560,16 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("45").WithPages(1).Build()) .WithChapter(new ChapterBuilder("46").WithPages(1).Build()) .WithChapter(new ChapterBuilder("47").WithPages(1).Build()) .WithChapter(new ChapterBuilder("48").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title") - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) - .WithIsSpecial(true).WithPages(1) - .Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) // One file volume .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) // Read + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) // Read .Build()) // Chapter-based volume .WithVolume(new VolumeBuilder("2") @@ -1586,14 +1584,14 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1615,7 +1613,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 3 // Volume 2 id }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1624,37 +1622,6 @@ public class ReaderServiceTests: AbstractDbTest } - - [Fact] - public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenHasSpecial() - { - await ResetDb(); - var series = new SeriesBuilder("Test") - // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) - .Build()) - .Build(); - series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - - Context.Series.Add(series); - - Context.AppUser.Add(new AppUser() - { - UserName = "majora2007" - }); - - await Context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetContinuePoint(1, 1); - - Assert.Equal("1", nextChapter.Range); - } - [Fact] public async Task GetContinuePoint_ShouldReturnFirstSpecial() { @@ -1667,21 +1634,21 @@ public class ReaderServiceTests: AbstractDbTest .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1709,7 +1676,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 2 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1721,7 +1688,7 @@ public class ReaderServiceTests: AbstractDbTest { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) .WithChapter(new ChapterBuilder("231").WithPages(1).Build()) .Build()) @@ -1736,15 +1703,15 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1757,39 +1724,37 @@ public class ReaderServiceTests: AbstractDbTest { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); var user = new AppUser() { UserName = "majora2007" }; - Context.AppUser.Add(user); + _context.AppUser.Add(user); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Mark everything but chapter 101 as read await _readerService.MarkSeriesAsRead(user, 1); - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); // Unmark last chapter as read - var vol = await UnitOfWork.VolumeRepository.GetVolumeByIdAsync(1); + var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1); foreach (var chapt in vol.Chapters) { await _readerService.SaveReadingProgress(new ProgressDto() @@ -1800,7 +1765,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 1 }, 1); } - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1812,7 +1777,7 @@ public class ReaderServiceTests: AbstractDbTest { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .WithChapter(new ChapterBuilder("102").WithPages(1).Build()) @@ -1828,22 +1793,22 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); var user = new AppUser() { UserName = "majora2007" }; - Context.AppUser.Add(user); + _context.AppUser.Add(user); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Mark everything but chapter 101 as read await _readerService.MarkSeriesAsRead(user, 1); - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); // Unmark last chapter as read - var vol = await UnitOfWork.VolumeRepository.GetVolumeByIdAsync(1); + var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1); await _readerService.SaveReadingProgress(new ProgressDto() { PageNum = 0, @@ -1858,7 +1823,7 @@ public class ReaderServiceTests: AbstractDbTest SeriesId = 1, VolumeId = 1 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1881,14 +1846,14 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -1915,7 +1880,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 2 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1928,7 +1893,7 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -1941,21 +1906,21 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Save progress on first volume chapters and 1st of second volume - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkSeriesAsRead(user, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1968,25 +1933,23 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); @@ -2013,7 +1976,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 1 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2026,7 +1989,7 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) //.WithChapter(new ChapterBuilder("231").WithPages(1).Build()) (Added later) .Build()) @@ -2036,26 +1999,26 @@ public class ReaderServiceTests: AbstractDbTest .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) //.WithChapter(new ChapterBuilder("14.9").WithPages(1).Build()) (added later) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkSeriesAsRead(user, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Add 2 new unread series to the Series series.Volumes[0].Chapters.Add(new ChapterBuilder("231") @@ -2064,8 +2027,8 @@ public class ReaderServiceTests: AbstractDbTest series.Volumes[2].Chapters.Add(new ChapterBuilder("14.9") .WithPages(1) .Build()); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); + _context.Series.Attach(series); + await _context.SaveChangesAsync(); // This tests that if you add a series later to a volume and a loose leaf chapter, we continue from that volume, rather than loose leaf var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2076,13 +2039,13 @@ public class ReaderServiceTests: AbstractDbTest public async Task GetContinuePoint_ShouldReturnUnreadSingleVolume_WhenThereAreSomeSingleVolumesBeforeLooseLeafChapters() { await ResetDb(); - var readChapter1 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var readChapter2 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()).Build(); + var readChapter1 = new ChapterBuilder("0").WithPages(1).Build(); + var readChapter2 = new ChapterBuilder("0").WithPages(1).Build(); + var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder("0").WithPages(1).Build()).Build(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("53").WithPages(1).Build()) @@ -2096,7 +2059,7 @@ public class ReaderServiceTests: AbstractDbTest .Build()) // 3, 4, and all loose leafs are unread should be unread .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("4") .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) @@ -2105,26 +2068,26 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Save progress on first volume chapters and 1st of second volume - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkChaptersAsRead(user, 1, new List() { readChapter1, readChapter2 }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2150,25 +2113,23 @@ public class ReaderServiceTests: AbstractDbTest .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("91").WithPages(2).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); await _readerService.SaveReadingProgress(new ProgressDto() { @@ -2226,7 +2187,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 2 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2255,14 +2216,14 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); await _readerService.SaveReadingProgress(new ProgressDto() { @@ -2272,7 +2233,7 @@ public class ReaderServiceTests: AbstractDbTest VolumeId = 1 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2287,7 +2248,7 @@ public class ReaderServiceTests: AbstractDbTest SeriesId = 1, VolumeId = 1 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2302,7 +2263,7 @@ public class ReaderServiceTests: AbstractDbTest SeriesId = 1, VolumeId = 1 }, 1); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2321,37 +2282,35 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 5); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); } [Fact] @@ -2360,39 +2319,37 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2.5").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) - .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 2.5f); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))); } [Fact] @@ -2402,32 +2359,31 @@ public class ReaderServiceTests: AbstractDbTest var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); - Assert.NotNull(user); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 2); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.True(await UnitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); + Assert.True(await _unitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); } [Fact] @@ -2436,23 +2392,21 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("45").WithPages(5).Build()) .WithChapter(new ChapterBuilder("46").WithPages(46).Build()) .WithChapter(new ChapterBuilder("47").WithPages(47).Build()) .WithChapter(new ChapterBuilder("48").WithPages(48).Build()) .WithChapter(new ChapterBuilder("49").WithPages(49).Build()) .WithChapter(new ChapterBuilder("50").WithPages(50).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(10).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(6).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(6).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(7).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(7).Build()) .Build()) .WithVolume(new VolumeBuilder("3") .WithChapter(new ChapterBuilder("12").WithPages(5).Build()) @@ -2462,24 +2416,24 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); const int markReadUntilNumber = 47; await _readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var volumes = await UnitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1); + var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1); Assert.True(volumes.SelectMany(v => v.Chapters).All(c => { // Specials are ignored. @@ -2502,35 +2456,35 @@ public class ReaderServiceTests: AbstractDbTest public async Task MarkSeriesAsReadTest() { await ResetDb(); - + // TODO: Validate this is correct, shouldn't be possible to have 2 Volume 0's in a series var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - await _readerService.MarkSeriesAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); - await Context.SaveChangesAsync(); + await _readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); + await _context.SaveChangesAsync(); - Assert.Equal(4, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + Assert.Equal(4, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); } @@ -2544,34 +2498,34 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var volumes = (await UnitOfWork.VolumeRepository.GetVolumes(1)).ToList(); - await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); + var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); + await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); - await Context.SaveChangesAsync(); - Assert.Equal(2, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + await _context.SaveChangesAsync(); + Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); - await _readerService.MarkSeriesAsUnread(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); - await Context.SaveChangesAsync(); + await _readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); + await _context.SaveChangesAsync(); - var progresses = (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; + var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; Assert.Equal(0, progresses.Max(p => p.PagesRead)); Assert.Equal(2, progresses.Count); } @@ -2617,54 +2571,51 @@ public class ReaderServiceTests: AbstractDbTest await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2002") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2003") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkVolumesUntilAsRead(user, 1, 2002); - Assert.NotNull(user); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate loose leaf chapters don't get marked as read - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); // Validate that volumes 1997 and 2002 both have their respective chapter 0 marked as read - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); // Validate that the chapter 0 of the following volume (2003) is not read - Assert.Null(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(7, 1)); + Assert.Null(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(7, 1)); } @@ -2673,13 +2624,11 @@ public class ReaderServiceTests: AbstractDbTest { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) @@ -2695,31 +2644,30 @@ public class ReaderServiceTests: AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - Context.Series.Add(series); + _context.Series.Add(series); - Context.AppUser.Add(new AppUser() + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); - Assert.NotNull(user); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkVolumesUntilAsRead(user, 1, 2002); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); // Validate loose leaf chapters don't get marked as read - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); // Validate volumes chapter 0 have read status - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))?.PagesRead); - Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1))?.PagesRead); - Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); } #endregion diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 7a6ed3e0b..c6d9675d0 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -11,11 +11,15 @@ using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; +using API.Services.Tasks; using API.SignalR; +using API.Tests.Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -48,9 +52,7 @@ public class ReadingListServiceTests var mapper = config.CreateMapper(); _unitOfWork = new UnitOfWork(_context, mapper, null!); - var ds = new DirectoryService(Substitute.For>(), new MockFileSystem()); - _readingListService = new ReadingListService(_unitOfWork, Substitute.For>(), - Substitute.For(), Substitute.For(), ds); + _readingListService = new ReadingListService(_unitOfWork, Substitute.For>(), Substitute.For()); _readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), @@ -126,7 +128,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -175,7 +177,7 @@ public class ReadingListServiceTests .WithSeries(new SeriesBuilder("Test") .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -234,7 +236,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -294,7 +296,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -373,7 +375,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -430,7 +432,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -495,7 +497,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .Build() ) @@ -536,7 +538,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .Build() ) @@ -579,93 +581,6 @@ public class ReadingListServiceTests Assert.Equal(AgeRating.G, readingList.AgeRating); } - [Fact] - public async Task UpdateReadingListAgeRatingForSeries() - { - await ResetDb(); - var spiceAndWolf = new SeriesBuilder("Spice and Wolf") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .WithVolumes([ - new VolumeBuilder("1") - .WithChapters([ - new ChapterBuilder("1").Build(), - new ChapterBuilder("2").Build(), - ]).Build() - ]).Build(); - spiceAndWolf.Metadata.AgeRating = AgeRating.Everyone; - - var othersidePicnic = new SeriesBuilder("Otherside Picnic ") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .WithVolumes([ - new VolumeBuilder("1") - .WithChapters([ - new ChapterBuilder("1").Build(), - new ChapterBuilder("2").Build(), - ]).Build() - ]).Build(); - othersidePicnic.Metadata.AgeRating = AgeRating.Everyone; - - _context.AppUser.Add(new AppUser() - { - UserName = "Amelia", - ReadingLists = new List(), - Libraries = new List - { - new LibraryBuilder("Test Library", LibraryType.LightNovel) - .WithSeries(spiceAndWolf) - .WithSeries(othersidePicnic) - .Build(), - }, - }); - - await _context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("Amelia", AppUserIncludes.ReadingLists); - Assert.NotNull(user); - - var myTestReadingList = new ReadingListBuilder("MyReadingList").Build(); - var mySecondTestReadingList = new ReadingListBuilder("MySecondReadingList").Build(); - var myThirdTestReadingList = new ReadingListBuilder("MyThirdReadingList").Build(); - user.ReadingLists = new List() - { - myTestReadingList, - mySecondTestReadingList, - myThirdTestReadingList, - }; - - - await _readingListService.AddChaptersToReadingList(spiceAndWolf.Id, new List {1, 2}, myTestReadingList); - await _readingListService.AddChaptersToReadingList(othersidePicnic.Id, new List {3, 4}, myTestReadingList); - await _readingListService.AddChaptersToReadingList(spiceAndWolf.Id, new List {1, 2}, myThirdTestReadingList); - await _readingListService.AddChaptersToReadingList(othersidePicnic.Id, new List {3, 4}, mySecondTestReadingList); - - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - await _readingListService.CalculateReadingListAgeRating(myTestReadingList); - await _readingListService.CalculateReadingListAgeRating(mySecondTestReadingList); - Assert.Equal(AgeRating.Everyone, myTestReadingList.AgeRating); - Assert.Equal(AgeRating.Everyone, mySecondTestReadingList.AgeRating); - Assert.Equal(AgeRating.Everyone, myThirdTestReadingList.AgeRating); - - await _readingListService.UpdateReadingListAgeRatingForSeries(othersidePicnic.Id, AgeRating.Mature); - await _unitOfWork.CommitAsync(); - - // Reading lists containing Otherside Picnic are updated - myTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1); - Assert.NotNull(myTestReadingList); - Assert.Equal(AgeRating.Mature, myTestReadingList.AgeRating); - - mySecondTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(2); - Assert.NotNull(mySecondTestReadingList); - Assert.Equal(AgeRating.Mature, mySecondTestReadingList.AgeRating); - - // Unrelated reading list is not updated - myThirdTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(3); - Assert.NotNull(myThirdTestReadingList); - Assert.Equal(AgeRating.Everyone, myThirdTestReadingList.AgeRating); - } - #endregion #region CalculateStartAndEndDates @@ -678,7 +593,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .Build() ) @@ -730,7 +645,7 @@ public class ReadingListServiceTests .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1") .WithReleaseDate(new DateTime(2005, 03, 01)) .Build() @@ -796,9 +711,6 @@ public class ReadingListServiceTests Assert.Equal("Issue #1", ReadingListService.FormatTitle(CreateListItemDto(MangaFormat.Archive, LibraryType.Comic, "1", "1", "The Title"))); Assert.Equal("Volume 1", ReadingListService.FormatTitle(CreateListItemDto(MangaFormat.Archive, LibraryType.Comic, "1", chapterTitleName: "The Title"))); Assert.Equal("The Title", ReadingListService.FormatTitle(CreateListItemDto(MangaFormat.Archive, LibraryType.Comic, chapterTitleName: "The Title"))); - var dto = CreateListItemDto(MangaFormat.Archive, LibraryType.Comic, chapterNumber: "The Special Title"); - dto.IsSpecial = true; - Assert.Equal("The Special Title", ReadingListService.FormatTitle(dto)); // Book Library & Archive Assert.Equal("Volume 1", ReadingListService.FormatTitle(CreateListItemDto(MangaFormat.Archive, LibraryType.Book, "1"))); @@ -824,8 +736,8 @@ public class ReadingListServiceTests } private static ReadingListItemDto CreateListItemDto(MangaFormat seriesFormat, LibraryType libraryType, - string volumeNumber = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, - string chapterNumber =API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + string volumeNumber = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, + string chapterNumber = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, string chapterTitleName = "") { return new ReadingListItemDto() @@ -847,7 +759,7 @@ public class ReadingListServiceTests var fablesSeries = new SeriesBuilder("Fables").Build(); fablesSeries.Volumes.Add( new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .Build() @@ -1025,7 +937,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -1033,7 +945,7 @@ public class ReadingListServiceTests .Build() ); fables2Series.Volumes.Add(new VolumeBuilder("1") - .WithMinNumber(1) + .WithNumber(1) .WithName("2003") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -1068,13 +980,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1124,7 +1036,7 @@ public class ReadingListServiceTests // Mock up our series var fablesSeries = new SeriesBuilder("Fables") .WithVolume(new VolumeBuilder("2002") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1133,7 +1045,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle") .WithVolume(new VolumeBuilder("2003") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1182,13 +1094,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1241,13 +1153,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithMinNumber(1) + .WithNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1293,65 +1205,6 @@ public class ReadingListServiceTests Assert.Equal(2, createdList.Items.First(item => item.Order == 2).ChapterId); Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId); } - - /// - /// This test is about ensuring Annuals that are a separate series can be linked up properly (ComicVine) - /// - //[Fact] - public async Task CreateReadingListFromCBL_ShouldCreateList_WithAnnuals() - { - // TODO: Implement this correctly - await ResetDb(); - var cblReadingList = LoadCblFromPath("Annual.cbl"); - - // Mock up our series - var fablesSeries = new SeriesBuilder("Fables") - .WithVolume(new VolumeBuilder("2002") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) - .WithChapter(new ChapterBuilder("3").Build()) - .Build()) - .Build(); - - var fables2Series = new SeriesBuilder("Fables Annual") - .WithVolume(new VolumeBuilder("2003") - .WithMinNumber(1) - .WithChapter(new ChapterBuilder("1").Build()) - .Build()) - .Build(); - - _context.AppUser.Add(new AppUser() - { - UserName = "majora2007", - ReadingLists = new List(), - Libraries = new List() - { - new LibraryBuilder("Test LIb 2", LibraryType.Book) - .WithSeries(fablesSeries) - .WithSeries(fables2Series) - .Build() - }, - }); - await _unitOfWork.CommitAsync(); - - var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList); - - Assert.Equal(CblImportResult.Success, importSummary.Success); - Assert.NotEmpty(importSummary.Results); - - var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1); - - Assert.NotNull(createdList); - Assert.Equal("Annual", createdList.Title); - - Assert.Equal(4, createdList.Items.Count); - Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId); - Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId); - Assert.Equal(4, createdList.Items.First(item => item.Order == 2).ChapterId); - Assert.Equal(3, createdList.Items.First(item => item.Order == 3).ChapterId); - } - #endregion #region CreateReadingListsFromSeries @@ -1386,7 +1239,7 @@ public class ReadingListServiceTests var series2 = new SeriesBuilder("Series 2") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) diff --git a/API.Tests/Services/ReadingProfileServiceTest.cs b/API.Tests/Services/ReadingProfileServiceTest.cs deleted file mode 100644 index b3d81e5ac..000000000 --- a/API.Tests/Services/ReadingProfileServiceTest.cs +++ /dev/null @@ -1,561 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Tests.Helpers; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class ReadingProfileServiceTest: AbstractDbTest -{ - - /// - /// Does not add a default reading profile - /// - /// - public async Task<(ReadingProfileService, AppUser, Library, Series)> Setup() - { - var user = new AppUserBuilder("amelia", "amelia@localhost").Build(); - Context.AppUser.Add(user); - await UnitOfWork.CommitAsync(); - - var series = new SeriesBuilder("Spice and Wolf").Build(); - - var library = new LibraryBuilder("Manga") - .WithSeries(series) - .Build(); - - user.Libraries.Add(library); - await UnitOfWork.CommitAsync(); - - var rps = new ReadingProfileService(UnitOfWork, Substitute.For(), Mapper); - user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.UserPreferences); - - return (rps, user, library, series); - } - - [Fact] - public async Task ImplicitProfileFirst() - { - await ResetDb(); - var (rps, user, library, series) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .WithSeries(series) - .WithName("Implicit Profile") - .Build(); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Non-implicit Profile") - .Build(); - - user.ReadingProfiles.Add(profile); - user.ReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal("Implicit Profile", seriesProfile.Name); - - // Find parent - seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id, true); - Assert.NotNull(seriesProfile); - Assert.Equal("Non-implicit Profile", seriesProfile.Name); - } - - [Fact] - public async Task CantDeleteDefaultReadingProfile() - { - await ResetDb(); - var (rps, user, _, _) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Default) - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await Assert.ThrowsAsync(async () => - { - await rps.DeleteReadingProfile(user.Id, profile.Id); - }); - - var profile2 = new AppUserReadingProfileBuilder(user.Id).Build(); - Context.AppUserReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - await rps.DeleteReadingProfile(user.Id, profile2.Id); - await UnitOfWork.CommitAsync(); - - var allProfiles = await Context.AppUserReadingProfiles.ToListAsync(); - Assert.Single(allProfiles); - } - - [Fact] - public async Task CreateImplicitSeriesReadingProfile() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.Webtoon, - ScalingOption = ScalingOption.FitToHeight, - WidthOverride = 53, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - } - - [Fact] - public async Task UpdateImplicitReadingProfile_DoesNotCreateNew() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.Webtoon, - ScalingOption = ScalingOption.FitToHeight, - WidthOverride = 53, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - - dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.LeftRight, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - Assert.Equal(ReaderMode.LeftRight, profile.ReaderMode); - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(1, implicitCount); - } - - [Fact] - public async Task GetCorrectProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Series Specific") - .Build(); - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithLibrary(lib) - .WithName("Library Specific") - .Build(); - var profile3 = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Default) - .WithName("Global") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - Context.AppUserReadingProfiles.Add(profile2); - Context.AppUserReadingProfiles.Add(profile3); - - var series2 = new SeriesBuilder("Rainbows After Storms").Build(); - lib.Series.Add(series2); - - var lib2 = new LibraryBuilder("Manga2").Build(); - var series3 = new SeriesBuilder("A Tropical Fish Yearns for Snow").Build(); - lib2.Series.Add(series3); - - user.Libraries.Add(lib2); - await UnitOfWork.CommitAsync(); - - var p = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(p); - Assert.Equal("Series Specific", p.Name); - - p = await rps.GetReadingProfileDtoForSeries(user.Id, series2.Id); - Assert.NotNull(p); - Assert.Equal("Library Specific", p.Name); - - p = await rps.GetReadingProfileDtoForSeries(user.Id, series3.Id); - Assert.NotNull(p); - Assert.Equal("Global", p.Name); - } - - [Fact] - public async Task ReplaceReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile1 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile 1") - .Build(); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 2") - .Build(); - - Context.AppUserReadingProfiles.Add(profile1); - Context.AppUserReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - var profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Equal("Profile 1", profile.Name); - - await rps.AddProfileToSeries(user.Id, profile2.Id, series.Id); - profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Equal("Profile 2", profile.Name); - } - - [Fact] - public async Task DeleteReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile1 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile 1") - .Build(); - - Context.AppUserReadingProfiles.Add(profile1); - await UnitOfWork.CommitAsync(); - - await rps.ClearSeriesProfile(user.Id, series.Id); - var profiles = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.DoesNotContain(profiles, rp => rp.SeriesIds.Contains(series.Id)); - - } - - [Fact] - public async Task BulkAddReadingProfiles() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - for (var i = 0; i < 10; i++) - { - var generatedSeries = new SeriesBuilder($"Generated Series #{i}").Build(); - lib.Series.Add(generatedSeries); - } - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile2") - .Build(); - Context.AppUserReadingProfiles.Add(profile2); - - await UnitOfWork.CommitAsync(); - - var someSeriesIds = lib.Series.Take(lib.Series.Count / 2).Select(s => s.Id).ToList(); - await rps.BulkAddProfileToSeries(user.Id, profile.Id, someSeriesIds); - - foreach (var id in someSeriesIds) - { - var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(foundProfile); - Assert.Equal(profile.Id, foundProfile.Id); - } - - var allIds = lib.Series.Select(s => s.Id).ToList(); - await rps.BulkAddProfileToSeries(user.Id, profile2.Id, allIds); - - foreach (var id in allIds) - { - var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(foundProfile); - Assert.Equal(profile2.Id, foundProfile.Id); - } - - - } - - [Fact] - public async Task BulkAssignDeletesImplicit() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var implicitProfile = Mapper.Map(new AppUserReadingProfileBuilder(user.Id) - .Build()); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 1") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - - for (var i = 0; i < 10; i++) - { - var generatedSeries = new SeriesBuilder($"Generated Series #{i}").Build(); - lib.Series.Add(generatedSeries); - } - await UnitOfWork.CommitAsync(); - - var ids = lib.Series.Select(s => s.Id).ToList(); - - foreach (var id in ids) - { - await rps.UpdateImplicitReadingProfile(user.Id, id, implicitProfile); - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); - } - - await rps.BulkAddProfileToSeries(user.Id, profile.Id, ids); - - foreach (var id in ids) - { - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - } - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(0, implicitCount); - } - - [Fact] - public async Task AddDeletesImplicit() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var implicitProfile = Mapper.Map(new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .Build()); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 1") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, implicitProfile); - - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); - - await rps.AddProfileToSeries(user.Id, profile.Id, series.Id); - - seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(0, implicitCount); - } - - [Fact] - public async Task CreateReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - Name = "Profile 1", - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await rps.CreateReadingProfile(user.Id, dto); - - var dto2 = new UserReadingProfileDto - { - Name = "Profile 2", - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await rps.CreateReadingProfile(user.Id, dto2); - - var dto3 = new UserReadingProfileDto - { - Name = "Profile 1", // Not unique name - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await Assert.ThrowsAsync(async () => - { - await rps.CreateReadingProfile(user.Id, dto3); - }); - - var allProfiles = Context.AppUserReadingProfiles.ToList(); - Assert.Equal(2, allProfiles.Count); - } - - [Fact] - public async Task ClearSeriesProfile_RemovesImplicitAndUnlinksExplicit() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var implicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithKind(ReadingProfileKind.Implicit) - .WithName("Implicit Profile") - .Build(); - - var explicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Explicit Profile") - .Build(); - - Context.AppUserReadingProfiles.Add(implicitProfile); - Context.AppUserReadingProfiles.Add(explicitProfile); - await UnitOfWork.CommitAsync(); - - var allBefore = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.Equal(2, allBefore.Count(rp => rp.SeriesIds.Contains(series.Id))); - - await rps.ClearSeriesProfile(user.Id, series.Id); - - var remainingProfiles = await Context.AppUserReadingProfiles.ToListAsync(); - Assert.Single(remainingProfiles); - Assert.Equal("Explicit Profile", remainingProfiles[0].Name); - Assert.Empty(remainingProfiles[0].SeriesIds); - - var profilesForSeries = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.DoesNotContain(profilesForSeries, rp => rp.SeriesIds.Contains(series.Id)); - } - - [Fact] - public async Task AddProfileToLibrary_AddsAndOverridesExisting() - { - await ResetDb(); - var (rps, user, lib, _) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Library Profile") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await rps.AddProfileToLibrary(user.Id, profile.Id, lib.Id); - await UnitOfWork.CommitAsync(); - - var linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.NotNull(linkedProfile); - Assert.Equal(profile.Id, linkedProfile.Id); - - var newProfile = new AppUserReadingProfileBuilder(user.Id) - .WithName("New Profile") - .Build(); - Context.AppUserReadingProfiles.Add(newProfile); - await UnitOfWork.CommitAsync(); - - await rps.AddProfileToLibrary(user.Id, newProfile.Id, lib.Id); - await UnitOfWork.CommitAsync(); - - linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.NotNull(linkedProfile); - Assert.Equal(newProfile.Id, linkedProfile.Id); - } - - [Fact] - public async Task ClearLibraryProfile_RemovesImplicitOrUnlinksExplicit() - { - await ResetDb(); - var (rps, user, lib, _) = await Setup(); - - var implicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .WithLibrary(lib) - .Build(); - Context.AppUserReadingProfiles.Add(implicitProfile); - await UnitOfWork.CommitAsync(); - - await rps.ClearLibraryProfile(user.Id, lib.Id); - var profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.Null(profile); - - var explicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithLibrary(lib) - .Build(); - Context.AppUserReadingProfiles.Add(explicitProfile); - await UnitOfWork.CommitAsync(); - - await rps.ClearLibraryProfile(user.Id, lib.Id); - profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.Null(profile); - - var stillExists = await Context.AppUserReadingProfiles.FindAsync(explicitProfile.Id); - Assert.NotNull(stillExists); - } - - /// - /// As response to #3793, I'm not sure if we want to keep this. It's not the most nice. But I think the idea of this test - /// is worth having. - /// - [Fact] - public void UpdateFields_UpdatesAll() - { - // Repeat to ensure booleans are flipped and actually tested - for (int i = 0; i < 10; i++) - { - var profile = new AppUserReadingProfile(); - var dto = new UserReadingProfileDto(); - - RandfHelper.SetRandomValues(profile); - RandfHelper.SetRandomValues(dto); - - ReadingProfileService.UpdateReaderProfileFields(profile, dto); - - var newDto = Mapper.Map(profile); - - Assert.True(RandfHelper.AreSimpleFieldsEqual(dto, newDto, - ["k__BackingField", "k__BackingField"])); - } - } - - - - protected override async Task ResetDb() - { - Context.AppUserReadingProfiles.RemoveRange(Context.AppUserReadingProfiles); - await UnitOfWork.CommitAsync(); - } -} diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index c337d2311..0d0277e3e 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,998 +1,71 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using API.Data.Metadata; -using API.Data.Repositories; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Extensions; +using API.Helpers.Builders; +using API.Services.Tasks; +using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; using API.Tests.Helpers; -using Hangfire; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Services; -public class ScannerServiceTests : AbstractDbTest +public class ScannerServiceTests { - private readonly ITestOutputHelper _testOutputHelper; - private readonly ScannerHelper _scannerHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); - - public ScannerServiceTests(ITestOutputHelper testOutputHelper) + [Fact] + public void FindSeriesNotOnDisk_Should_Remove1() { - _testOutputHelper = testOutputHelper; + var infos = new Dictionary>(); - // Set up Hangfire to use in-memory storage for testing - GlobalConfiguration.Configuration.UseInMemoryStorage(); - _scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper); - } + ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive}); + //AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub}); - protected override async Task ResetDb() - { - Context.Library.RemoveRange(Context.Library); - await Context.SaveChangesAsync(); - } - - - protected async Task SetAllSeriesLastScannedInThePast(Library library, TimeSpan? duration = null) - { - foreach (var series in library.Series) + var existingSeries = new List { - await SetLastScannedInThePast(series, duration, false); - } - await Context.SaveChangesAsync(); + new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Epub) + + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build() + }; + + Assert.Single(ScannerService.FindSeriesNotOnDisk(existingSeries, infos)); } - protected async Task SetLastScannedInThePast(Series series, TimeSpan? duration = null, bool save = true) + [Fact] + public void FindSeriesNotOnDisk_Should_RemoveNothing_Test() { - duration ??= TimeSpan.FromMinutes(2); - series.LastFolderScanned = DateTime.Now.Subtract(duration.Value); - Context.Series.Update(series); + var infos = new Dictionary>(); - if (save) + ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Format = MangaFormat.Archive}); + ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Cage of Eden", Volumes = "1", Format = MangaFormat.Archive}); + ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Cage of Eden", Volumes = "10", Format = MangaFormat.Archive}); + + var existingSeries = new List { - await Context.SaveChangesAsync(); - } - } - - [Fact] - public async Task ScanLibrary_ComicVine_PublisherFolder() - { - var testcase = "Publisher - ComicVine.json"; - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Equal(4, postLib.Series.Count); - } - - [Fact] - public async Task ScanLibrary_ShouldCombineNestedFolder() - { - var testcase = "Series and Series-Series Combined - Manga.json"; - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(2, postLib.Series.First().Volumes.Count); - } - - - [Fact] - public async Task ScanLibrary_FlatSeries() - { - const string testcase = "Flat Series - Manga.json"; - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(3, postLib.Series.First().Volumes.Count); - - // TODO: Trigger a deletion of ch 10 - } - - [Fact] - public async Task ScanLibrary_FlatSeriesWithSpecialFolder() - { - const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json"; - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(4, postLib.Series.First().Volumes.Count); - Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null)); - } - - [Fact] - public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming() - { - const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json"; - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(4, postLib.Series.First().Volumes.Count); - Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null)); - } - - [Fact] - public async Task ScanLibrary_FlatSeriesWithSpecial() - { - const string testcase = "Flat Special - Manga.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(3, postLib.Series.First().Volumes.Count); - Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null)); - } - - [Fact] - public async Task ScanLibrary_SeriesWithUnbalancedParenthesis() - { - const string testcase = "Scan Library Parses as ( - Manga.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var series = postLib.Series.First(); - - Assert.Equal("Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", series.Name); - } - - /// - /// This is testing that if the first file is named A and has a localized name of B if all other files are named B, it should still group and name the series A - /// - [Fact] - public async Task ScanLibrary_LocalizedSeries() - { - const string testcase = "Series with Localized - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("My Dress-Up Darling v01.cbz", new ComicInfo() - { - Series = "My Dress-Up Darling", - LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru" - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(3, postLib.Series.First().Volumes.Count); - } - - [Fact] - public async Task ScanLibrary_LocalizedSeries2() - { - const string testcase = "Series with Localized 2 - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Immoral Guild v01.cbz", new ComicInfo() - { - Series = "Immoral Guild", - LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal("Immoral Guild", s.Name); - Assert.Equal("Futoku no Guild", s.LocalizedName); - Assert.Equal(3, s.Volumes.Count); - } - - - /// - /// Special Keywords shouldn't be removed from the series name and thus these 2 should group - /// - [Fact] - public async Task ScanLibrary_ExtraShouldNotAffect() - { - const string testcase = "Series with Extra - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Vol.01.cbz", new ComicInfo() - { - Series = "The Novel's Extra", - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal("The Novel's Extra", s.Name); - Assert.Equal(2, s.Volumes.Count); - } - - - /// - /// Files under a folder with a SP marker should group into one issue - /// - /// https://github.com/Kareadita/Kavita/issues/3299 - [Fact] - public async Task ScanLibrary_ImageSeries_SpecialGrouping() - { - const string testcase = "Image Series with SP Folder - Manga.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Equal(3, postLib.Series.First().Volumes.Count); - } - - /// - /// This test is currently disabled because the Image parser is unable to support multiple files mapping into one single Special. - /// https://github.com/Kareadita/Kavita/issues/3299 - /// - public async Task ScanLibrary_ImageSeries_SpecialGrouping_NonEnglish() - { - const string testcase = "Image Series with SP Folder (Non English) - Image.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var series = postLib.Series.First(); - Assert.Equal(3, series.Volumes.Count); - var specialVolume = series.Volumes.FirstOrDefault(v => v.Name == Parser.SpecialVolume); - Assert.NotNull(specialVolume); - Assert.Single(specialVolume.Chapters); - Assert.True(specialVolume.Chapters.First().IsSpecial); - //Assert.Equal("葬送のフリーレン 公式ファンブック SP01", specialVolume.Chapters.First().Title); - } - - - [Fact] - public async Task ScanLibrary_PublishersInheritFromChapters() - { - const string testcase = "Flat Special - Manga.json"; - - var infos = new Dictionary(); - infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo() - { - Publisher = "Correct Publisher" - }); - infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo() - { - Publisher = "Special Publisher" - }); - infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo() - { - Publisher = "Chapter Publisher" - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var publishers = postLib.Series.First().Metadata.People - .Where(p => p.Role == PersonRole.Publisher); - Assert.Equal(3, publishers.Count()); - } - - - /// - /// Tests that pdf parser handles the loose chapters correctly - /// https://github.com/Kareadita/Kavita/issues/3148 - /// - [Fact] - public async Task ScanLibrary_LooseChapters_Pdf() - { - const string testcase = "PDF Comic Chapters - Comic.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var series = postLib.Series.First(); - Assert.Single(series.Volumes); - Assert.Equal(4, series.Volumes.First().Chapters.Count); - } - - [Fact] - public async Task ScanLibrary_LooseChapters_Pdf_LN() - { - const string testcase = "PDF Comic Chapters - LightNovel.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var series = postLib.Series.First(); - Assert.Single(series.Volumes); - Assert.Equal(4, series.Volumes.First().Chapters.Count); - } - - /// - /// This is the same as doing ScanFolder as the case where it can find the series is just ScanSeries - /// - [Fact] - public async Task ScanSeries_NewChapterInNestedFolder() - { - const string testcase = "Series with Localized - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("My Dress-Up Darling v01.cbz", new ComicInfo() - { - Series = "My Dress-Up Darling", - LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru" - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var series = postLib.Series.First(); - Assert.Equal(3, series.Volumes.Count); - - // Bootstrap a new file in the nested "Sono Bisque Doll wa Koi wo Suru" directory and perform a series scan - var testDirectory = Path.Combine(_testDirectory, Path.GetFileNameWithoutExtension(testcase)); - await _scannerHelper.Scaffold(testDirectory, ["My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 11.cbz"]); - - // Now that a new file exists in the subdirectory, scan again - await scanner.ScanSeries(series.Id); - Assert.Single(postLib.Series); - Assert.Equal(3, series.Volumes.Count); - Assert.Equal(2, series.Volumes.First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)).Chapters.Count); - } - - [Fact] - public async Task ScanLibrary_LocalizedSeries_MatchesFilename() - { - const string testcase = "Localized Name matches Filename - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Futoku no Guild v01.cbz", new ComicInfo() - { - Series = "Immoral Guild", - LocalizedSeries = "Futoku no Guild" - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal("Immoral Guild", s.Name); - Assert.Equal("Futoku no Guild", s.LocalizedName); - Assert.Single(s.Volumes); - } - - [Fact] - public async Task ScanLibrary_LocalizedSeries_MatchesFilename_SameNames() - { - const string testcase = "Localized Name matches Filename - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Futoku no Guild v01.cbz", new ComicInfo() - { - Series = "Futoku no Guild", - LocalizedSeries = "Futoku no Guild" - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal("Futoku no Guild", s.Name); - Assert.Equal("Futoku no Guild", s.LocalizedName); - Assert.Single(s.Volumes); - } - - [Fact] - public async Task ScanLibrary_ExcludePattern_Works() - { - const string testcase = "Exclude Pattern 1 - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**/Extra/*" }]; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal(2, s.Volumes.Count); - } - - [Fact] - public async Task ScanLibrary_ExcludePattern_FlippedSlashes_Works() - { - const string testcase = "Exclude Pattern 1 - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**\\Extra\\*" }]; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var s = postLib.Series.First(); - Assert.Equal(2, s.Volumes.Count); - } - - [Fact] - public async Task ScanLibrary_MultipleRoots_MultipleScans_DataPersists_Forced() - { - const string testcase = "Multiple Roots - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - var testDirectoryPath = - Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), - testcase.Replace(".json", string.Empty)); - library.Folders = - [ - new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 1")}, - new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 2")} - ]; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Equal(2, postLib.Series.Count); - var s = postLib.Series.First(s => s.Name == "Plush"); - Assert.Equal(2, s.Volumes.Count); - var s2 = postLib.Series.First(s => s.Name == "Accel"); - Assert.Single(s2.Volumes); - - // Make a change (copy a file into only 1 root) - var root1PlushFolder = Path.Join(testDirectoryPath, "Root 1/Antarctic Press/Plush"); - File.Copy(Path.Join(root1PlushFolder, "Plush v02.cbz"), Path.Join(root1PlushFolder, "Plush v03.cbz")); - - // Rescan to ensure nothing changes yet again - await scanner.ScanLibrary(library.Id, true); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.Equal(2, postLib.Series.Count); - s = postLib.Series.First(s => s.Name == "Plush"); - Assert.Equal(3, s.Volumes.Count); - s2 = postLib.Series.First(s => s.Name == "Accel"); - Assert.Single(s2.Volumes); - } - - /// - /// Regression bug appeared where multi-root and one root gets a new file, on next scan of library, - /// the series in the other root are deleted. (This is actually failing because the file in Root 1 isn't being detected) - /// - [Fact] - public async Task ScanLibrary_MultipleRoots_MultipleScans_DataPersists_NonForced() - { - const string testcase = "Multiple Roots - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - var testDirectoryPath = - Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), - testcase.Replace(".json", string.Empty)); - library.Folders = - [ - new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 1")}, - new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 2")} - ]; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Equal(2, postLib.Series.Count); - var s = postLib.Series.First(s => s.Name == "Plush"); - Assert.Equal(2, s.Volumes.Count); - var s2 = postLib.Series.First(s => s.Name == "Accel"); - Assert.Single(s2.Volumes); - - // Make a change (copy a file into only 1 root) - var root1PlushFolder = Path.Join(testDirectoryPath, "Root 1/Antarctic Press/Plush"); - File.Copy(Path.Join(root1PlushFolder, "Plush v02.cbz"), Path.Join(root1PlushFolder, "Plush v03.cbz")); - - // Emulate time passage by updating lastFolderScan to be a min in the past - await SetLastScannedInThePast(s); - - // Rescan to ensure nothing changes yet again - await scanner.ScanLibrary(library.Id, false); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.Equal(2, postLib.Series.Count); - s = postLib.Series.First(s => s.Name == "Plush"); - Assert.Equal(3, s.Volumes.Count); - s2 = postLib.Series.First(s => s.Name == "Accel"); - Assert.Single(s2.Volumes); - } - - [Fact] - public async Task ScanLibrary_AlternatingRemoval_IssueReplication() - { - // https://github.com/Kareadita/Kavita/issues/3476#issuecomment-2661635558 - const string testcase = "Alternating Removal - Manga.json"; - - // Setup: Generate test library - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", - testcase.Replace(".json", string.Empty)); - - library.Folders = - [ - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }, - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } - ]; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - - // First Scan: Everything should be added - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Contains(postLib.Series, s => s.Name == "Accel"); - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - - // Second Scan: Remove Root 2, expect Accel to be removed - library.Folders = [new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }]; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - // Emulate time passage by updating lastFolderScan to be a min in the past - foreach (var s in postLib.Series) - { - s.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(1)); - Context.Series.Update(s); - } - await Context.SaveChangesAsync(); - - await scanner.ScanLibrary(library.Id); - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.DoesNotContain(postLib.Series, s => s.Name == "Accel"); // Ensure Accel is gone - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - - // Third Scan: Re-add Root 2, Accel should come back - library.Folders = - [ - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }, - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } - ]; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - // Emulate time passage by updating lastFolderScan to be a min in the past - foreach (var s in postLib.Series) - { - s.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(1)); - Context.Series.Update(s); - } - await Context.SaveChangesAsync(); - - await scanner.ScanLibrary(library.Id); - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.Contains(postLib.Series, s => s.Name == "Accel"); // Accel should be back - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - - // Emulate time passage by updating lastFolderScan to be a min in the past - await SetAllSeriesLastScannedInThePast(postLib); - - // Fourth Scan: Run again to check stability (should not remove Accel) - await scanner.ScanLibrary(library.Id); - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.Contains(postLib.Series, s => s.Name == "Accel"); - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - } - - [Fact] - public async Task ScanLibrary_DeleteSeriesInUI_ComeBack() - { - const string testcase = "Delete Series In UI - Manga.json"; - - // Setup: Generate test library - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", - testcase.Replace(".json", string.Empty)); - - library.Folders = - [ - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }, - new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } - ]; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - - // First Scan: Everything should be added - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Contains(postLib.Series, s => s.Name == "Accel"); - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - - // Second Scan: Delete the Series - library.Series = []; - await UnitOfWork.CommitAsync(); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Empty(postLib.Series); - - await scanner.ScanLibrary(library.Id); - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.Contains(postLib.Series, s => s.Name == "Accel"); // Ensure Accel is gone - Assert.Contains(postLib.Series, s => s.Name == "Plush"); - } - - [Fact] - public async Task SubFolders_NoRemovals_ChangesFound() - { - const string testcase = "Subfolders always scanning all series changes - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Equal(4, postLib.Series.Count); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(2, spiceAndWolf.Volumes.Count); - Assert.Equal(3, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); - Assert.Single(frieren.Volumes); - Assert.Equal(2, frieren.Volumes.Sum(v => v.Chapters.Count)); - - var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life"); - Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count); - Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Sum(v => v.Chapters.Count)); - - await SetAllSeriesLastScannedInThePast(postLib); - - // Add a new chapter to a volume of the series, and scan. Validate that no chapters were lost, and the new - // chapter was added - var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "The Executioner and Her Way of Life"), - "The Executioner and Her Way of Life Vol. 1"); - File.Copy(Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0001.cbz"), - Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0002.cbz")); - - await scanner.ScanLibrary(library.Id); - await UnitOfWork.CommitAsync(); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Equal(4, postLib.Series.Count); - - spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(2, spiceAndWolf.Volumes.Count); - Assert.Equal(3, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); - Assert.Single(frieren.Volumes); - Assert.Equal(2, frieren.Volumes.Sum(v => v.Chapters.Count)); - - executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life"); - Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count); - Assert.Equal(3, executionerAndHerWayOfLife.Volumes.Sum(v => v.Chapters.Count)); // Incremented by 1 - } - - [Fact] - public async Task RemovalPickedUp_NoOtherChanges() - { - const string testcase = "Series removed when no other changes are made - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Equal(2, postLib.Series.Count); - - var executionerCopyDir = Path.Join(testDirectoryPath, "The Executioner and Her Way of Life"); - Directory.Delete(executionerCopyDir, true); - - await scanner.ScanLibrary(library.Id); - await UnitOfWork.CommitAsync(); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - Assert.Single(postLib.Series, s => s.Name == "Spice and Wolf"); - Assert.Equal(2, postLib.Series.First().Volumes.Count); - } - - [Fact] - public async Task SubFoldersNoSubFolders_CorrectPickupAfterAdd() - { - // This test case is used in multiple tests and can result in conflict if not separated - const string testcase = "Subfolders and files at root (2) - Manga.json"; - var infos = new Dictionary(); - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - var testDirectoryPath = library.Folders.First().Path; - - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - var spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(3, spiceAndWolf.Volumes.Count); - Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - await SetLastScannedInThePast(spiceAndWolf); - - // Add volume to Spice and Wolf series directory - var spiceAndWolfDir = Path.Join(testDirectoryPath, "Spice and Wolf"); - File.Copy(Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 1.cbz"), - Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 4.cbz")); - - await scanner.ScanLibrary(library.Id); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(4, spiceAndWolf.Volumes.Count); - Assert.Equal(5, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - await SetLastScannedInThePast(spiceAndWolf); - - // Add file in subfolder - spiceAndWolfDir = Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3"); - File.Copy(Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3 Ch. 0012.cbz"), - Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3 Ch. 0013.cbz")); - - await scanner.ScanLibrary(library.Id); - - postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - - spiceAndWolf = postLib.Series.First(x => x.Name == "Spice and Wolf"); - Assert.Equal(4, spiceAndWolf.Volumes.Count); - Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); - - } - - - /// - /// Ensure when Kavita scans, the sort order of chapters is correct - /// - [Fact] - public async Task ScanLibrary_SortOrderWorks() - { - const string testcase = "Sort Order - Manga.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - - // Get the loose leaf volume and confirm each chapter aligns with expectation of Sort Order - var series = postLib.Series.First(); - Assert.NotNull(series); - - var volume = series.Volumes.FirstOrDefault(); - Assert.NotNull(volume); - - var sortedChapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList(); - Assert.True(sortedChapters[0].SortOrder.Is(1f)); - Assert.True(sortedChapters[1].SortOrder.Is(4f)); - Assert.True(sortedChapters[2].SortOrder.Is(5f)); - } - - - [Fact] - public async Task ScanLibrary_MetadataDisabled_NoOverrides() - { - const string testcase = "Series with Localized No Metadata - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Immoral Guild v01.cbz", new ComicInfo() - { - Series = "Immoral Guild", - LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - // Disable metadata - library.EnableMetadata = false; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - // Validate that there are 2 series - Assert.NotNull(postLib); - Assert.Equal(2, postLib.Series.Count); - - Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild"); - Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild"); - } - - [Fact] - public async Task ScanLibrary_SortName_NoPrefix() - { - const string testcase = "Series with Prefix - Book.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - library.RemovePrefixForSortName = true; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Equal(1, postLib.Series.Count); - - Assert.Equal("The Avengers", postLib.Series.First().Name); - Assert.Equal("Avengers", postLib.Series.First().SortName); + new SeriesBuilder("Cage of Eden") + .WithFormat(MangaFormat.Archive) + + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(), + new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(), + }; + + Assert.Empty(ScannerService.FindSeriesNotOnDisk(existingSeries, infos)); } } diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/API.Tests/Services/ScrobblingServiceTests.cs deleted file mode 100644 index 9245c8ecd..000000000 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ /dev/null @@ -1,632 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Scrobble; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.SignalR; -using Kavita.Common; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; -#nullable enable - -public class ScrobblingServiceTests : AbstractDbTest -{ - private const int ChapterPages = 100; - - /// - /// { - /// "Issuer": "Issuer", - /// "Issued At": "2025-06-15T21:01:57.615Z", - /// "Expiration": "2200-06-15T21:01:57.615Z" - /// } - /// - /// Our UnitTests will fail in 2200 :( - private const string ValidJwtToken = - "eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJleHAiOjcyNzI0NTAxMTcsImlhdCI6MTc1MDAyMTMxN30.zADmcGq_BfxbcV8vy4xw5Cbzn4COkmVINxgqpuL17Ng"; - - private readonly ScrobblingService _service; - private readonly ILicenseService _licenseService; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - private readonly IEmailService _emailService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; - /// - /// IReaderService, without the ScrobblingService injected - /// - private readonly IReaderService _readerService; - /// - /// IReaderService, with the _service injected - /// - private readonly IReaderService _hookedUpReaderService; - - public ScrobblingServiceTests() - { - _licenseService = Substitute.For(); - _localizationService = Substitute.For(); - _logger = Substitute.For>(); - _emailService = Substitute.For(); - _kavitaPlusApiService = Substitute.For(); - - _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, - _localizationService, _emailService, _kavitaPlusApiService); - - _readerService = new ReaderService(UnitOfWork, - Substitute.For>(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); // Do not use the actual one - - _hookedUpReaderService = new ReaderService(UnitOfWork, - Substitute.For>(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - _service); - } - - protected override async Task ResetDb() - { - Context.ScrobbleEvent.RemoveRange(Context.ScrobbleEvent.ToList()); - Context.Series.RemoveRange(Context.Series.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); - Context.AppUser.RemoveRange(Context.AppUser.ToList()); - - await UnitOfWork.CommitAsync(); - } - - private async Task SeedData() - { - var series = new SeriesBuilder("Test Series") - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder().Build()) - .WithVolume(new VolumeBuilder("Volume 1") - .WithChapters([ - new ChapterBuilder("1") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("2") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("3") - .WithPages(ChapterPages) - .Build()]) - .Build()) - .WithVolume(new VolumeBuilder("Volume 2") - .WithChapters([ - new ChapterBuilder("4") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("5") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("6") - .WithPages(ChapterPages) - .Build()]) - .Build()) - .Build(); - - var library = new LibraryBuilder("Test Library", LibraryType.Manga) - .WithAllowScrobbling(true) - .WithSeries(series) - .Build(); - - - Context.Library.Add(library); - - var user = new AppUserBuilder("testuser", "testuser") - //.WithPreferences(new UserPreferencesBuilder().WithAniListScrobblingEnabled(true).Build()) - .Build(); - - user.UserPreferences.AniListScrobblingEnabled = true; - - UnitOfWork.UserRepository.Add(user); - - await UnitOfWork.CommitAsync(); - } - - private async Task CreateScrobbleEvent(int? seriesId = null) - { - var evt = new ScrobbleEvent - { - ScrobbleEventType = ScrobbleEventType.ChapterRead, - Format = PlusMediaFormat.Manga, - SeriesId = seriesId ?? 0, - LibraryId = 0, - AppUserId = 0, - }; - - if (seriesId != null) - { - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value); - if (series != null) evt.Series = series; - } - - return evt; - } - - - #region K+ API Request Tests - - [Fact] - public async Task PostScrobbleUpdate_AuthErrors() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Unauthorized" - }); - - var evt = await CreateScrobbleEvent(); - await Assert.ThrowsAsync(async () => - { - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - }); - Assert.True(evt.IsErrored); - Assert.Equal("Kavita+ subscription no longer active", evt.ErrorDetails); - } - - [Fact] - public async Task PostScrobbleUpdate_UnknownSeriesLoggedAsError() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Unknown Series" - }); - - await SeedData(); - var evt = await CreateScrobbleEvent(1); - - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - await UnitOfWork.CommitAsync(); - Assert.True(evt.IsErrored); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); - Assert.True(series.IsBlacklisted); - - var errors = await UnitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(1); - Assert.Single(errors); - Assert.Equal("Series cannot be matched for Scrobbling", errors.First().Comment); - Assert.Equal(series.Id, errors.First().SeriesId); - } - - [Fact] - public async Task PostScrobbleUpdate_InvalidAccessToken() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Access token is invalid" - }); - - var evt = await CreateScrobbleEvent(); - - await Assert.ThrowsAsync(async () => - { - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - }); - - Assert.True(evt.IsErrored); - Assert.Equal("Access Token needs to be rotated to continue scrobbling", evt.ErrorDetails); - } - - #endregion - - #region K+ API Request data tests - - [Fact] - public async Task ProcessReadEvents_CreatesNoEventsWhenNoProgress() - { - await ResetDb(); - await SeedData(); - - // Set Returns - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) - .Returns(100); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - // Ensure CanProcessScrobbleEvent returns true - user.AniListAccessToken = ValidJwtToken; - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); - Assert.NotNull(chapter); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - // Call Scrobble without having any progress - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ProcessReadEvents_UpdateVolumeAndChapterData() - { - await ResetDb(); - await SeedData(); - - // Set Returns - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) - .Returns(100); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - // Ensure CanProcessScrobbleEvent returns true - user.AniListAccessToken = ValidJwtToken; - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); - Assert.NotNull(chapter); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - // Mark something as read to trigger event creation - await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); - await UnitOfWork.CommitAsync(); - - // Call Scrobble while having some progress - await _service.ScrobbleReadingUpdate(user.Id, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - // Give it some (more) read progress - await _readerService.MarkChaptersAsRead(user, 1, volume.Chapters); - await _readerService.MarkChaptersAsRead(user, 1, [chapter]); - await UnitOfWork.CommitAsync(); - - await _service.ProcessUpdatesSinceLastSync(); - - await _kavitaPlusApiService.Received(1).PostScrobbleUpdate( - Arg.Is(data => - data.ChapterNumber == (int)chapter.MaxNumber && - data.VolumeNumber == (int)volume.MaxNumber - ), - Arg.Any()); - } - - #endregion - - #region Scrobble Reading Update Tests - - [Fact] - public async Task ScrobbleReadingUpdate_IgnoreNoLicense() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(false); - - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleReadingUpdate_RemoveWhenNoProgress() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(true); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); - await UnitOfWork.CommitAsync(); - - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - var readEvent = events.First(); - Assert.False(readEvent.IsProcessed); - - await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); - await UnitOfWork.CommitAsync(); - - // Existing event is deleted - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - - await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); - await UnitOfWork.CommitAsync(); - - // No new events are added - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleReadingUpdate_UpdateExistingNotIsProcessed() - { - await ResetDb(); - await SeedData(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var chapter1 = await UnitOfWork.ChapterRepository.GetChapterAsync(1); - var chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2); - var chapter3 = await UnitOfWork.ChapterRepository.GetChapterAsync(3); - Assert.NotNull(chapter1); - Assert.NotNull(chapter2); - Assert.NotNull(chapter3); - - _licenseService.HasActiveLicense().Returns(true); - - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - - - await _readerService.MarkChaptersAsRead(user, 1, [chapter1]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - var readEvent = events[0]; - Assert.False(readEvent.IsProcessed); - Assert.Equal(1, readEvent.ChapterNumber); - - // Mark as processed - readEvent.IsProcessed = true; - await UnitOfWork.CommitAsync(); - - await _readerService.MarkChaptersAsRead(user, 1, [chapter2]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events.Where(e => e.IsProcessed).ToList()); - Assert.Single(events.Where(e => !e.IsProcessed).ToList()); - - // Should update the existing non processed event - await _readerService.MarkChaptersAsRead(user, 1, [chapter3]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events.Where(e => e.IsProcessed).ToList()); - Assert.Single(events.Where(e => !e.IsProcessed).ToList()); - } - - #endregion - - #region ScrobbleWantToReadUpdate Tests - - [Fact] - public async Task ScrobbleWantToReadUpdate_NoExistingEvents_WantToRead_ShouldCreateNewEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // Act - await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - Assert.Single(events); - Assert.Equal(ScrobbleEventType.AddWantToRead, events[0].ScrobbleEventType); - Assert.Equal(userId, events[0].AppUserId); - } - - [Fact] - public async Task ScrobbleWantToReadUpdate_NoExistingEvents_RemoveWantToRead_ShouldCreateNewEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // Act - await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - Assert.Single(events); - Assert.Equal(ScrobbleEventType.RemoveWantToRead, events[0].ScrobbleEventType); - Assert.Equal(userId, events[0].AppUserId); - } - - [Fact] - public async Task ScrobbleWantToReadUpdate_ExistingWantToReadEvent_WantToRead_ShouldNotCreateNewEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // First, let's create an event through the service - await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); - - // Act - Try to create the same event again - await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - - Assert.Single(events); - Assert.All(events, e => Assert.Equal(ScrobbleEventType.AddWantToRead, e.ScrobbleEventType)); - } - - [Fact] - public async Task ScrobbleWantToReadUpdate_ExistingWantToReadEvent_RemoveWantToRead_ShouldAddRemoveEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // First, let's create a want-to-read event through the service - await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); - - // Act - Now remove from want-to-read - await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - - Assert.Single(events); - Assert.Contains(events, e => e.ScrobbleEventType == ScrobbleEventType.RemoveWantToRead); - } - - [Fact] - public async Task ScrobbleWantToReadUpdate_ExistingRemoveWantToReadEvent_RemoveWantToRead_ShouldNotCreateNewEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // First, let's create a remove-from-want-to-read event through the service - await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); - - // Act - Try to create the same event again - await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - - Assert.Single(events); - Assert.All(events, e => Assert.Equal(ScrobbleEventType.RemoveWantToRead, e.ScrobbleEventType)); - } - - [Fact] - public async Task ScrobbleWantToReadUpdate_ExistingRemoveWantToReadEvent_WantToRead_ShouldAddWantToReadEvent() - { - // Arrange - await SeedData(); - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - - const int userId = 1; - const int seriesId = 1; - - // First, let's create a remove-from-want-to-read event through the service - await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); - - // Act - Now add to want-to-read - await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); - - // Assert - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - - Assert.Single(events); - Assert.Contains(events, e => e.ScrobbleEventType == ScrobbleEventType.AddWantToRead); - } - - #endregion - - #region Scrobble Rating Update Test - - [Fact] - public async Task ScrobbleRatingUpdate_IgnoreNoLicense() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(false); - - await _service.ScrobbleRatingUpdate(1, 1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleRatingUpdate_UpdateExistingNotIsProcessed() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(true); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - Assert.Equal(1, events.First().Rating); - - // Mark as processed - events.First().IsProcessed = true; - await UnitOfWork.CommitAsync(); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events, evt => evt.IsProcessed); - Assert.Single(events, evt => !evt.IsProcessed); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events, evt => !evt.IsProcessed); - Assert.Equal(5, events.First(evt => !evt.IsProcessed).Rating); - - } - - #endregion - - [Theory] - [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] - [InlineData("https://anilist.co/manga/30105", 30105)] - [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] - public void CanParseWeblink_AniList(string link, int? expectedId) - { - Assert.Equal(ScrobblingService.ExtractId(link, ScrobblingService.AniListWeblinkWebsite), expectedId); - } - - [Theory] - [InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")] - public void CanParseWeblink_MangaDex(string link, string expectedId) - { - Assert.Equal(ScrobblingService.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); - } -} diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 55babf815..199b303a9 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -1,30 +1,29 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.CollectionTags; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; -using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; +using API.Tests.Helpers; using Hangfire; using Hangfire.InMemory; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -49,36 +48,38 @@ public class SeriesServiceTests : AbstractDbTest { private readonly ISeriesService _seriesService; - public SeriesServiceTests() + public SeriesServiceTests() : base() { - var ds = new DirectoryService(Substitute.For>(), new FileSystem()); + var ds = new DirectoryService(Substitute.For>(), new FileSystem() + { + + }); var locService = new LocalizationService(ds, new MockHostingEnvironment(), Substitute.For(), Substitute.For()); - _seriesService = new SeriesService(UnitOfWork, Substitute.For(), + _seriesService = new SeriesService(_unitOfWork, Substitute.For(), Substitute.For(), Substitute.For>(), - Substitute.For(), locService, Substitute.For()); + Substitute.For(), locService); } - #region Setup protected override async Task ResetDb() { - Context.Series.RemoveRange(Context.Series.ToList()); - Context.AppUserRating.RemoveRange(Context.AppUserRating.ToList()); - Context.Genre.RemoveRange(Context.Genre.ToList()); - Context.CollectionTag.RemoveRange(Context.CollectionTag.ToList()); - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); + _context.Series.RemoveRange(_context.Series.ToList()); + _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); + _context.Genre.RemoveRange(_context.Genre.ToList()); + _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); + _context.Person.RemoveRange(_context.Person.ToList()); + _context.Library.RemoveRange(_context.Library.ToList()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); } private static UpdateRelatedSeriesDto CreateRelationsDto(Series series) { - return new UpdateRelatedSeriesDto + return new UpdateRelatedSeriesDto() { SeriesId = series.Id, Prequels = new List(), @@ -92,8 +93,7 @@ public class SeriesServiceTests : AbstractDbTest AlternativeVersions = new List(), SideStories = new List(), SpinOffs = new List(), - Editions = new List(), - Annuals = new List() + Editions = new List() }; } @@ -106,13 +106,13 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithTitle("Omake").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithTitle("Something").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) @@ -127,7 +127,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var expectedRanges = new[] {"Omake", "Something SP02"}; @@ -142,11 +142,11 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) @@ -163,7 +163,7 @@ public class SeriesServiceTests : AbstractDbTest .Build() ); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -179,16 +179,16 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("3") @@ -197,7 +197,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -213,15 +213,15 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("3") @@ -230,7 +230,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -249,22 +249,22 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Volumes); @@ -278,31 +278,29 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub") - .WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub") - .WithPages(1).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build()) + .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Volumes); Assert.Equal("2 - Ano Orokamono ni mo Kyakkou wo! - Volume 2", detail.Volumes.ElementAt(0).Name); Assert.NotEmpty(detail.Specials); - Assert.Equal("Ano Orokamono ni mo Kyakkou wo! - Volume 1", detail.Specials.ElementAt(0).Range); + Assert.Equal("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", detail.Specials.ElementAt(0).Range); // A book library where all books are Volumes, will show no "chapters" on the UI because it doesn't make sense Assert.Empty(detail.Chapters); @@ -315,25 +313,25 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Manga) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1.2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.Equal("Volume 1", detail.Volumes.ElementAt(0).Name); @@ -342,255 +340,164 @@ public class SeriesServiceTests : AbstractDbTest } - /// - /// Validates that the Series Detail API returns Title names as expected for Manga library type - /// + #endregion + + + #region UpdateRating + [Fact] - public async Task SeriesDetail_Manga_ShouldReturnAppropriatelyNamedTitles() + public async Task UpdateRating_ShouldSetRating() { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Manga) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithSortOrder(1).WithTitle("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2-5").WithSortOrder(2).WithTitle("2-5").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("5.5").WithSortOrder(3).WithTitle("5.5").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); - var detail = await _seriesService.GetSeriesDetail(1, 1); - Assert.NotEmpty(detail.Specials); + await _context.SaveChangesAsync(); - Assert.Equal("Volume 2", detail.Volumes.First().Name); - Assert.Equal("Volume 3", detail.Volumes.Last().Name); - var chapters = detail.Chapters.ToArray(); - Assert.Equal("Chapter 1", chapters[0].Title); - Assert.Equal("Chapter 2-5", chapters[1].Title); - Assert.Equal("Chapter 5.5", chapters[2].Title); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - Assert.Equal("Omake", detail.Specials.First().Title); - Assert.Equal("Something", detail.Specials.Last().Title); + JobStorage.Current = new InMemoryStorage(); + var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + { + SeriesId = 1, + UserRating = 3, + }); + + Assert.True(result); + + var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))! + .Ratings; + Assert.NotEmpty(ratings); + Assert.Equal(3, ratings.First().Rating); } - - /// - /// Validates that the Series Detail API returns Title names as expected for Comic library type - /// [Fact] - public async Task SeriesDetail_Comic_ShouldReturnAppropriatelyNamedTitles() + public async Task UpdateRating_ShouldUpdateExistingRating() { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Comic) + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithSortOrder(1).WithTitle("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2-5").WithSortOrder(2).WithTitle("2-5").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("5.5").WithSortOrder(3).WithTitle("5.5").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); - var detail = await _seriesService.GetSeriesDetail(1, 1); - Assert.NotEmpty(detail.Specials); + await _context.SaveChangesAsync(); - Assert.Equal("Volume 2", detail.Volumes.First().Name); - Assert.Equal("Volume 3", detail.Volumes.Last().Name); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - var chapters = detail.Chapters.ToArray(); - Assert.Equal("Issue #1", chapters[0].Title); - Assert.Equal("Issue #2-5", chapters[1].Title); - Assert.Equal("Issue #5.5", chapters[2].Title); + var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + { + SeriesId = 1, + UserRating = 3, + }); - Assert.Equal("Omake", detail.Specials.First().Title); - Assert.Equal("Something", detail.Specials.Last().Title); + Assert.True(result); + + JobStorage.Current = new InMemoryStorage(); + var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) + .Ratings; + Assert.NotEmpty(ratings); + Assert.Equal(3, ratings.First().Rating); + + // Update the DB again + + var result2 = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + { + SeriesId = 1, + UserRating = 5, + }); + + Assert.True(result2); + + var ratings2 = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) + .Ratings; + Assert.NotEmpty(ratings2); + Assert.True(ratings2.Count == 1); + Assert.Equal(5, ratings2.First().Rating); } - /// - /// Validates that the Series Detail API returns Title names as expected for ComicVine library type - /// [Fact] - public async Task SeriesDetail_ComicVine_ShouldReturnAppropriatelyNamedTitles() + public async Task UpdateRating_ShouldClampRatingAt5() { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.ComicVine) + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithSortOrder(1).WithTitle("Batman is Here").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2-5").WithSortOrder(2).WithTitle("Batman Left").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("5.5").WithSortOrder(3).WithTitle("Batman is Back").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var detail = await _seriesService.GetSeriesDetail(1, 1); - Assert.NotEmpty(detail.Specials); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - Assert.Equal("Volume 2", detail.Volumes.First().Name); - Assert.Equal("Volume 3", detail.Volumes.Last().Name); + var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + { + SeriesId = 1, + UserRating = 10, + }); - var chapters = detail.Chapters.ToArray(); - Assert.Equal("Issue #1 - Batman is Here", chapters[0].Title); - Assert.Equal("Issue #2-5 - Batman Left", chapters[1].Title); - Assert.Equal("Issue #5.5 - Batman is Back", chapters[2].Title); + Assert.True(result); - Assert.Equal("Omake", detail.Specials.First().Title); - Assert.Equal("Something", detail.Specials.Last().Title); + JobStorage.Current = new InMemoryStorage(); + var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", + AppUserIncludes.Ratings)) + .Ratings; + Assert.NotEmpty(ratings); + Assert.Equal(5, ratings.First().Rating); } - /// - /// Validates that the Series Detail API returns Title names as expected for Book library type - /// [Fact] - public async Task SeriesDetail_Book_ShouldReturnAppropriatelyNamedTitles() + public async Task UpdateRating_ShouldReturnFalseWhenSeriesDoesntExist() { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithSortOrder(1).WithTitle("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2-5").WithSortOrder(2).WithTitle("2-5").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("5.5").WithSortOrder(3).WithTitle("5.5").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithRange("Stone").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithRange("Paper").WithPages(1).Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .Build()) .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var detail = await _seriesService.GetSeriesDetail(1, 1); - Assert.NotEmpty(detail.Specials); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); - Assert.Equal("2 - Stone", detail.Volumes.First().Name); - Assert.Equal("3 - Paper", detail.Volumes.Last().Name); + var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto() + { + SeriesId = 2, + UserRating = 5, + }); - var chapters = detail.StorylineChapters.ToArray(); - Assert.Equal("Book 1", chapters[0].Title); - Assert.Equal("Book 2-5", chapters[1].Title); - Assert.Equal("Book 5.5", chapters[2].Title); + Assert.False(result); - Assert.Equal("Omake", detail.Specials.First().Title); - Assert.Equal("Something", detail.Specials.Last().Title); + var ratings = user.Ratings; + Assert.Empty(ratings); } - /// - /// Validates that the Series Detail API returns Title names as expected for LightNovel library type - /// - [Fact] - public async Task SeriesDetail_LightNovel_ShouldReturnAppropriatelyNamedTitles() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.LightNovel) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithSortOrder(1).WithTitle("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2-5").WithSortOrder(2).WithTitle("2-5").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("5.5").WithSortOrder(3).WithTitle("5.5").WithPages(1).Build()) - .Build()) - .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithRange("Stone").WithPages(1).Build()) - .Build()) - - .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithRange("Paper").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - await Context.SaveChangesAsync(); - - - var detail = await _seriesService.GetSeriesDetail(1, 1); - Assert.NotEmpty(detail.Specials); - - Assert.Equal("2 - Stone", detail.Volumes.First().Name); - Assert.Equal("3 - Paper", detail.Volumes.Last().Name); - - var chapters = detail.StorylineChapters.ToArray(); - Assert.Equal("Book 1", chapters[0].Title); - Assert.Equal("Book 2-5", chapters[1].Title); - Assert.Equal("Book 5.5", chapters[2].Title); - - Assert.Equal("Omake", detail.Specials.First().Title); - Assert.Equal("Something", detail.Specials.Last().Title); - } - - - #endregion #region UpdateSeriesMetadata @@ -603,25 +510,64 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - Context.Series.Add(s); - await Context.SaveChangesAsync(); + _context.Series.Add(s); + await _context.SaveChangesAsync(); - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Genres = new List {new GenreTagDto {Id = 0, Title = "New Genre"}} + Genres = new List() {new GenreTagDto() {Id = 0, Title = "New Genre"}} }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); + + } + + [Fact] + public async Task UpdateSeriesMetadata_ShouldCreateNewTags_IfNoneExist() + { + await ResetDb(); + var s = new SeriesBuilder("Test") + .Build(); + s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); + + _context.Series.Add(s); + await _context.SaveChangesAsync(); + + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() + { + SeriesMetadata = new SeriesMetadataDto() + { + SeriesId = 1, + Genres = new List() {new GenreTagDto() {Id = 0, Title = "New Genre"}}, + Tags = new List() {new TagDto() {Id = 0, Title = "New Tag"}}, + Characters = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Character}}, + Colorists = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Colorist}}, + Pencillers = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo 2", Role = PersonRole.Penciller}}, + }, + CollectionTags = new List() + { + new CollectionTagDto() {Id = 0, Promoted = false, Summary = string.Empty, CoverImageLocked = false, Title = "New Collection"} + } + }); + + Assert.True(success); + + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series.Metadata); + Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); + Assert.True(series.Metadata.People.All(g => g.Name is "Joe Shmo" or "Joe Shmo 2")); + Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(g => g.Title)); + Assert.Contains("New Collection", series.Metadata.CollectionTags.Select(g => g.Title)); + } [Fact] @@ -634,26 +580,25 @@ public class SeriesServiceTests : AbstractDbTest s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var g = new GenreBuilder("Existing Genre").Build(); - s.Metadata.Genres = new List {g}; - Context.Series.Add(s); + s.Metadata.Genres = new List() {g}; + _context.Series.Add(s); - Context.Genre.Add(g); - await Context.SaveChangesAsync(); + _context.Genre.Add(g); + await _context.SaveChangesAsync(); - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Genres = new List {new () {Id = 0, Title = "New Genre"}}, + Genres = new List() {new () {Id = 0, Title = "New Genre"}}, }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase())); Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked @@ -663,38 +608,32 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople() { await ResetDb(); - var g = new PersonBuilder("Existing Person").Build(); - await Context.SaveChangesAsync(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(g, PersonRole.Publisher) - .Build()) + .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); + var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build(); + _context.Series.Add(s); - Context.Series.Add(s); + _context.Person.Add(g); + await _context.SaveChangesAsync(); - Context.Person.Add(g); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Publishers = new List {new () {Id = 0, Name = "Existing Person"}}, + Publishers = new List() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}}, }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); - Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); + Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person")); Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked } @@ -706,96 +645,34 @@ public class SeriesServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - var g = new PersonBuilder("Existing Person").Build(); - s.Metadata.People = new List + var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build(); + s.Metadata.People = new List() {new PersonBuilder("Existing Writer", PersonRole.Writer).Build(), + new PersonBuilder("Existing Translator", PersonRole.Translator).Build(), new PersonBuilder("Existing Publisher 2", PersonRole.Publisher).Build()}; + _context.Series.Add(s); + + _context.Person.Add(g); + await _context.SaveChangesAsync(); + + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Writer").Build(), Role = PersonRole.Writer}, - new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Translator").Build(), Role = PersonRole.Translator}, - new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Publisher 2").Build(), Role = PersonRole.Publisher} - }; - - Context.Series.Add(s); - - Context.Person.Add(g); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Publishers = new List {new () {Id = 0, Name = "Existing Person"}}, - PublisherLocked = true + Publishers = new List() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}}, + PublishersLocked = true }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); - Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); + Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person")); Assert.True(series.Metadata.PublisherLocked); } - /// - /// I'm not sure how I could handle this use-case - /// - //[Fact] - public async Task UpdateSeriesMetadata_ShouldUpdate_ExistingPeople_NewName() - { - await ResetDb(); // Resets the database for a clean state - - // Arrange: Build series, metadata, and existing people - var series = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - series.Library = new LibraryBuilder("Test Library", LibraryType.Book).Build(); - - var existingPerson = new PersonBuilder("Existing Person").Build(); - var existingWriter = new PersonBuilder("ExistingWriter").Build(); // Pre-existing writer - - series.Metadata.People = new List - { - new SeriesMetadataPeople { Person = existingWriter, Role = PersonRole.Writer }, - new SeriesMetadataPeople { Person = new PersonBuilder("Existing Translator").Build(), Role = PersonRole.Translator }, - new SeriesMetadataPeople { Person = new PersonBuilder("Existing Publisher 2").Build(), Role = PersonRole.Publisher } - }; - - Context.Series.Add(series); - Context.Person.Add(existingPerson); - await Context.SaveChangesAsync(); - - // Act: Update series metadata, attempting to update the writer to "Existing Writer" - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = series.Id, // Use the series ID - Writers = new List { new() { Id = 0, Name = "Existing Writer" } }, // Trying to update writer's name - WriterLocked = true - } - }); - - // Assert: Ensure the operation was successful - Assert.True(success); - - // Reload the series from the database - var updatedSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id); - Assert.NotNull(updatedSeries.Metadata); - - // Assert that the people list still contains the updated person with the new name - var updatedPerson = updatedSeries.Metadata.People.FirstOrDefault(p => p.Role == PersonRole.Writer)?.Person; - Assert.NotNull(updatedPerson); // Make sure the person exists - Assert.Equal("Existing Writer", updatedPerson.Name); // Check if the person's name was updated - - // Assert that the publisher lock is still true - Assert.True(updatedSeries.Metadata.WriterLocked); - } - - [Fact] public async Task UpdateSeriesMetadata_ShouldRemoveExistingPerson() { @@ -804,83 +681,29 @@ public class SeriesServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - var g = new PersonBuilder("Existing Person").Build(); - Context.Series.Add(s); + var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build(); + _context.Series.Add(s); - Context.Person.Add(g); - await Context.SaveChangesAsync(); + _context.Person.Add(g); + await _context.SaveChangesAsync(); - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Publishers = new List(), + Publishers = new List() {}, }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); Assert.False(series.Metadata.People.Any()); } - /// - /// This emulates the UI operations wrt to locking - /// - [Fact] - public async Task UpdateSeriesMetadata_ShouldRemoveExistingPerson_AfterAdding() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - var g = new PersonBuilder("Existing Person").Build(); - Context.Series.Add(s); - - Context.Person.Add(g); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = 1, - Publishers = new List() {new PersonDto() {Name = "Test"}}, - PublisherLocked = true - }, - - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.True(series.Metadata.People.Count != 0); - Assert.True(series.Metadata.PublisherLocked); - - - success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = 1, - Publishers = new List(), - PublisherLocked = false - }, - - }); - - Assert.True(success); - Assert.Empty(series.Metadata.People); - Assert.False(series.Metadata.PublisherLocked); - } - [Fact] public async Task UpdateSeriesMetadata_ShouldLockIfTold() { @@ -890,28 +713,27 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var g = new GenreBuilder("Existing Genre").Build(); - s.Metadata.Genres = new List {g}; + s.Metadata.Genres = new List() {g}; s.Metadata.GenresLocked = true; - Context.Series.Add(s); + _context.Series.Add(s); - Context.Genre.Add(g); - await Context.SaveChangesAsync(); + _context.Genre.Add(g); + await _context.SaveChangesAsync(); - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, - Genres = new List {new () {Id = 1, Title = "Existing Genre"}}, + Genres = new List() {new () {Id = 1, Title = "Existing Genre"}}, GenresLocked = true }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase())); Assert.True(series.Metadata.GenresLocked); @@ -925,23 +747,22 @@ public class SeriesServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - Context.Series.Add(s); - await Context.SaveChangesAsync(); + _context.Series.Add(s); + await _context.SaveChangesAsync(); - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto + var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() { - SeriesMetadata = new SeriesMetadataDto + SeriesMetadata = new SeriesMetadataDto() { SeriesId = 1, ReleaseYear = 100, }, - + CollectionTags = new List() }); Assert.True(success); - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series.Metadata); Assert.Equal(0, series.Metadata.ReleaseYear); Assert.False(series.Metadata.ReleaseYearLocked); @@ -949,205 +770,6 @@ public class SeriesServiceTests : AbstractDbTest #endregion - #region UpdateGenres - [Fact] - public async Task UpdateSeriesMetadata_ShouldAddNewGenre_NoExistingGenres() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - Context.Series.Add(s); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Genres = new List {new () {Id = 0, Title = "New Genre"}}, - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); - Assert.False(series.Metadata.GenresLocked); // Ensure the lock is not activated unless specified. - } - - [Fact] - public async Task UpdateSeriesMetadata_ShouldReplaceExistingGenres() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - var g = new GenreBuilder("Existing Genre").Build(); - s.Metadata.Genres = new List { g }; - - Context.Series.Add(s); - Context.Genre.Add(g); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Genres = new List { new() { Id = 0, Title = "New Genre" }}, - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); - Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); - } - - [Fact] - public async Task UpdateSeriesMetadata_ShouldRemoveAllGenres() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - var g = new GenreBuilder("Existing Genre").Build(); - s.Metadata.Genres = new List { g }; - - Context.Series.Add(s); - Context.Genre.Add(g); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Genres = new List(), // Removing all genres - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.Empty(series.Metadata.Genres); - } - - #endregion - - #region UpdateTags - [Fact] - public async Task UpdateSeriesMetadata_ShouldAddNewTag_NoExistingTags() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - Context.Series.Add(s); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Tags = new List { new() { Id = 0, Title = "New Tag" }}, - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); - } - - [Fact] - public async Task UpdateSeriesMetadata_ShouldReplaceExistingTags() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - var t = new TagBuilder("Existing Tag").Build(); - s.Metadata.Tags = new List { t }; - - Context.Series.Add(s); - Context.Tag.Add(t); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Tags = new List { new() { Id = 0, Title = "New Tag" }}, - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); - Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); - } - - [Fact] - public async Task UpdateSeriesMetadata_ShouldRemoveAllTags() - { - await ResetDb(); - var s = new SeriesBuilder("Test") - .WithMetadata(new SeriesMetadataBuilder().Build()) - .Build(); - s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - - var t = new TagBuilder("Existing Tag").Build(); - s.Metadata.Tags = new List { t }; - - Context.Series.Add(s); - Context.Tag.Add(t); - await Context.SaveChangesAsync(); - - var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto - { - SeriesMetadata = new SeriesMetadataDto - { - SeriesId = s.Id, - Tags = new List(), // Removing all tags - }, - }); - - Assert.True(success); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); - Assert.NotNull(series); - Assert.NotNull(series.Metadata); - Assert.Empty(series.Metadata.Tags); - } - - #endregion - #region GetFirstChapterForMetadata private static Series CreateSeriesMock() @@ -1155,12 +777,10 @@ public class SeriesServiceTests : AbstractDbTest var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).WithFile(file).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).WithFile(file).Build()) - .Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A Special Case").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithFile(file).WithPages(1).Build()) + .WithChapter(new ChapterBuilder("A Special Case").WithIsSpecial(true).WithFile(file).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build()) @@ -1185,21 +805,20 @@ public class SeriesServiceTests : AbstractDbTest [Fact] public void GetFirstChapterForMetadata_BookWithOnlyVolumeNumbers_Test() { - var file = new MangaFileBuilder("Test.cbz", MangaFormat.Epub, 1).Build(); + var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build(); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(1).WithFile(file).Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") - .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(2).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(2).WithFile(file).Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); - Assert.NotNull(firstChapter); Assert.Equal(1, firstChapter.Pages); } @@ -1209,8 +828,6 @@ public class SeriesServiceTests : AbstractDbTest var series = CreateSeriesMock(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); - Assert.NotNull(firstChapter); - Assert.NotNull(firstChapter); Assert.Same("1", firstChapter.Range); } @@ -1220,19 +837,18 @@ public class SeriesServiceTests : AbstractDbTest var series = CreateSeriesMock(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); - Assert.NotNull(firstChapter); - Assert.Equal(1, firstChapter.MinNumber); + Assert.Same("1", firstChapter.Range); } [Fact] public void GetFirstChapterForMetadata_NonBook_ShouldReturnVolume1_WhenFirstChapterIsFloat() { var series = CreateSeriesMock(); - var files = new List + var files = new List() { new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build() }; - series.Volumes[2].Chapters = new List + series.Volumes[1].Chapters = new List() { new ChapterBuilder("2").WithFiles(files).WithPages(1).Build(), new ChapterBuilder("1.1").WithFiles(files).WithPages(1).Build(), @@ -1240,8 +856,7 @@ public class SeriesServiceTests : AbstractDbTest }; var firstChapter = SeriesService.GetFirstChapterForMetadata(series); - Assert.NotNull(firstChapter); - Assert.True(firstChapter.MinNumber.Is(1.1f)); + Assert.Same("1.1", firstChapter.Range); } [Fact] @@ -1250,7 +865,7 @@ public class SeriesServiceTests : AbstractDbTest var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).WithFile(file).Build()) .Build()) @@ -1266,29 +881,28 @@ public class SeriesServiceTests : AbstractDbTest series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); - Assert.NotNull(firstChapter); - Assert.Equal(1, firstChapter.MinNumber); + Assert.Same("1", firstChapter.Range); } #endregion - #region Series Relation + #region SeriesRelation [Fact] public async Task UpdateRelatedSeries_ShouldAddAllRelations() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Test Series").Build(), new SeriesBuilder("Test Series Prequels").Build(), @@ -1296,71 +910,34 @@ public class SeriesServiceTests : AbstractDbTest } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); addRelationDto.Sequels.Add(3); await _seriesService.UpdateRelatedSeries(addRelationDto); - Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId); } - [Fact] - public async Task UpdateRelatedSeries_ShouldAddPrequelWhenAddingSequel() - { - await ResetDb(); - Context.Library.Add(new Library - { - AppUsers = new List - { - new AppUser - { - UserName = "majora2007" - } - }, - Name = "Test LIb", - Type = LibraryType.Book, - Series = new List - { - new SeriesBuilder("Test Series").Build(), - new SeriesBuilder("Test Series Prequels").Build(), - } - }); - - await Context.SaveChangesAsync(); - - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); - var series2 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related); - // Add relations - var addRelationDto = CreateRelationsDto(series1); - addRelationDto.Sequels.Add(2); - await _seriesService.UpdateRelatedSeries(addRelationDto); - Assert.NotNull(series1); - Assert.NotNull(series2); - Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); - Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId); - } - [Fact] public async Task UpdateRelatedSeries_DeleteAllRelations() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Test Series").Build(), new SeriesBuilder("Test Series Prequels").Build(), @@ -1368,24 +945,22 @@ public class SeriesServiceTests : AbstractDbTest } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); addRelationDto.Sequels.Add(3); await _seriesService.UpdateRelatedSeries(addRelationDto); - Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId); // Remove relations var removeRelationDto = CreateRelationsDto(series1); await _seriesService.UpdateRelatedSeries(removeRelationDto); - Assert.NotNull(series1); - Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 1); - Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 2); + Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 1)); + Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2)); } @@ -1393,39 +968,37 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateRelatedSeries_DeleteTargetSeries_ShouldSucceed() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Series A").Build(), new SeriesBuilder("Series B").Build(), } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); - - Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); - Context.Series.Remove(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); try { - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); } catch (Exception) { @@ -1433,47 +1006,44 @@ public class SeriesServiceTests : AbstractDbTest } // Remove relations - Assert.Empty((await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related)).Relations); + Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related)).Relations); } [Fact] public async Task UpdateRelatedSeries_DeleteSourceSeries_ShouldSucceed() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Series A").Build(), new SeriesBuilder("Series B").Build(), } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); - Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); - var seriesToRemove = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(seriesToRemove); - Context.Series.Remove(seriesToRemove); + _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); try { - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); } catch (Exception) { @@ -1481,35 +1051,35 @@ public class SeriesServiceTests : AbstractDbTest } // Remove relations - Assert.Empty((await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related)).Relations); + Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related)).Relations); } [Fact] public async Task UpdateRelatedSeries_ShouldNotAllowDuplicates() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Test Series").Build(), new SeriesBuilder("Test Series Prequels").Build(), } }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); - var relation = new SeriesRelation + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var relation = new SeriesRelation() { Series = series1, SeriesId = series1.Id, @@ -1533,18 +1103,18 @@ public class SeriesServiceTests : AbstractDbTest public async Task GetRelatedSeries_EditionPrequelSequel_ShouldNotHaveParent() { await ResetDb(); - Context.Library.Add(new Library + _context.Library.Add(new Library() { - AppUsers = new List + AppUsers = new List() { - new AppUser + new AppUser() { UserName = "majora2007" } }, Name = "Test LIb", Type = LibraryType.Book, - Series = new List + Series = new List() { new SeriesBuilder("Test Series").Build(), new SeriesBuilder("Test Series Editions").Build(), @@ -1553,8 +1123,8 @@ public class SeriesServiceTests : AbstractDbTest new SeriesBuilder("Test Series Adaption").Build(), } }); - await Context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + await _context.SaveChangesAsync(); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Editions.Add(2); @@ -1580,30 +1150,30 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - Context.Library.Add(lib); + _context.Library.Add(lib); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); addRelationDto.Sequels.Add(3); await _seriesService.UpdateRelatedSeries(addRelationDto); - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id); - UnitOfWork.LibraryRepository.Delete(library); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id); + _unitOfWork.LibraryRepository.Delete(library); try { - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); } catch (Exception) { Assert.False(true); } - Assert.Null(await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); + Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); } [Fact] @@ -1612,7 +1182,7 @@ public class SeriesServiceTests : AbstractDbTest await ResetDb(); var lib1 = new LibraryBuilder("Test LIb") .WithSeries(new SeriesBuilder("Test Series") - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("1").WithFile( new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive) .WithPages(1) @@ -1624,37 +1194,37 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - Context.Library.Add(lib1); + _context.Library.Add(lib1); var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book) .WithSeries(new SeriesBuilder("Test Series 2").Build()) .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build()) - .WithSeries(new SeriesBuilder("Test Series Prequels 3").Build())// TODO: Is this a bug + .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build())// TODO: Is this a bug .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - Context.Library.Add(lib2); + _context.Library.Add(lib2); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(4); // cross library link await _seriesService.UpdateRelatedSeries(addRelationDto); - var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(lib1.Id, LibraryIncludes.Series); - UnitOfWork.LibraryRepository.Delete(library); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib1.Id, LibraryIncludes.Series); + _unitOfWork.LibraryRepository.Delete(library); try { - await UnitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(); } catch (Exception) { Assert.False(true); } - Assert.Null(await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); + Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); } #endregion @@ -1676,354 +1246,149 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - Context.Library.Add(new LibraryBuilder("Test LIb") + _context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty) .WithLocale("en") .Build()) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); Assert.Equal(expected, await _seriesService.FormatChapterName(1, libraryType, withHash)); } #endregion - // This is now handled in SeriesDetail Tests - // #region FormatChapterTitle - // - // [Fact] - // public async Task FormatChapterTitle_Manga_NonSpecial() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); - // Assert.Equal("Chapter Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Manga_Special() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build(); - // Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Comic_NonSpecial_WithoutHash() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); - // Assert.Equal("Issue Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Comic_Special_WithoutHash() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build(); - // Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Comic_NonSpecial_WithHash() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); - // Assert.Equal("Issue #Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Comic_Special_WithHash() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build(); - // Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Book_NonSpecial() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); - // Assert.Equal("Book Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false)); - // } - // - // [Fact] - // public async Task FormatChapterTitle_Book_Special() - // { - // await ResetDb(); - // - // _context.Library.Add(new LibraryBuilder("Test LIb") - // .WithAppUser(new AppUserBuilder("majora2007", string.Empty) - // .WithLocale("en") - // .Build()) - // .Build()); - // - // await _context.SaveChangesAsync(); - // var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build(); - // Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false)); - // } - // - // #endregion - - #region DeleteMultipleSeries + #region FormatChapterTitle [Fact] - public async Task DeleteMultipleSeries_ShouldDeleteSeries() + public async Task FormatChapterTitle_Manga_NonSpecial() { await ResetDb(); - var lib1 = new LibraryBuilder("Test LIb") - .WithSeries(new SeriesBuilder("Test Series") - .WithMetadata(new SeriesMetadata - { - AgeRating = AgeRating.Everyone - }) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithFile( - new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive) - .WithPages(1) - .Build() - ).Build()) - .Build()) + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") .Build()) - .WithSeries(new SeriesBuilder("Test Series Prequels").Build()) - .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .Build(); - Context.Library.Add(lib1); + .Build()); - var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book) - .WithSeries(new SeriesBuilder("Test Series 2").Build()) - .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build()) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .Build(); - Context.Library.Add(lib2); + await _context.SaveChangesAsync(); - await Context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); + Assert.Equal("Chapter Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false)); + } - var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, - SeriesIncludes.Related | SeriesIncludes.ExternalRatings); - // Add relations - var addRelationDto = CreateRelationsDto(series1); - addRelationDto.Adaptations.Add(4); // cross library link - await _seriesService.UpdateRelatedSeries(addRelationDto); + [Fact] + public async Task FormatChapterTitle_Manga_Special() + { + await ResetDb(); + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); - // Setup External Metadata stuff - series1.ExternalSeriesMetadata ??= new ExternalSeriesMetadata(); - series1.ExternalSeriesMetadata.ExternalRatings = new List - { - new ExternalRating - { - SeriesId = 1, - Provider = ScrobbleProvider.Mal, - AverageScore = 1 - } - }; - series1.ExternalSeriesMetadata.ExternalRecommendations = new List - { - new ExternalRecommendation - { - SeriesId = 2, - Name = "Series 2", - Url = "", - CoverUrl = "" - }, - new ExternalRecommendation - { - SeriesId = 0, // Causes a FK constraint - Name = "Series 2", - Url = "", - CoverUrl = "" - } - }; - series1.ExternalSeriesMetadata.ExternalReviews = new List - { - new ExternalReview - { - Body = "", - Provider = ScrobbleProvider.Mal, - BodyJustText = "" - } - }; + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); + Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false)); + } - await Context.SaveChangesAsync(); + [Fact] + public async Task FormatChapterTitle_Comic_NonSpecial_WithoutHash() + { + await ResetDb(); - // Ensure we can delete the series - Assert.True(await _seriesService.DeleteMultipleSeries(new[] {1, 2})); - Assert.Null(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); - Assert.Null(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); + Assert.Equal("Issue Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false)); + } + + [Fact] + public async Task FormatChapterTitle_Comic_Special_WithoutHash() + { + await ResetDb(); + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); + Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false)); + } + + [Fact] + public async Task FormatChapterTitle_Comic_NonSpecial_WithHash() + { + await ResetDb(); + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); + Assert.Equal("Issue #Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true)); + } + + [Fact] + public async Task FormatChapterTitle_Comic_Special_WithHash() + { + await ResetDb(); + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); + Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true)); + } + + [Fact] + public async Task FormatChapterTitle_Book_NonSpecial() + { + await ResetDb(); + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build(); + Assert.Equal("Book Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false)); + } + + [Fact] + public async Task FormatChapterTitle_Book_Special() + { + await ResetDb(); + + _context.Library.Add(new LibraryBuilder("Test LIb") + .WithAppUser(new AppUserBuilder("majora2007", string.Empty) + .WithLocale("en") + .Build()) + .Build()); + + await _context.SaveChangesAsync(); + var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); + Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false)); } #endregion - - #region GetEstimatedChapterCreationDate - - [Fact] - public async Task GetEstimatedChapterCreationDate_NoNextChapter_InvalidType() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); - Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); - Assert.Equal(0, nextChapter.ChapterNumber); - } - - [Fact] - public async Task GetEstimatedChapterCreationDate_NoNextChapter_InvalidPublicationStatus() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb") - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - .WithPublicationStatus(PublicationStatus.Completed) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); - Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); - Assert.Equal(0, nextChapter.ChapterNumber); - } - - [Fact] - public async Task GetEstimatedChapterCreationDate_NoNextChapter_Only2Chapters() - { - await ResetDb(); - - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); - Assert.NotNull(nextChapter); - Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); - Assert.Equal(0, nextChapter.ChapterNumber); - } - - [Fact] - public async Task GetEstimatedChapterCreationDate_NextChapter_ChaptersMonthApart() - { - await ResetDb(); - var now = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture); // 10/31/2024 can trigger an edge case bug - - Context.Library.Add(new LibraryBuilder("Test LIb") - .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) - .WithSeries(new SeriesBuilder("Test") - .WithPublicationStatus(PublicationStatus.OnGoing) - .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder("1").WithCreated(now).WithPages(1).Build()) - .WithChapter(new ChapterBuilder("2").WithCreated(now.AddMonths(1)).WithPages(1).Build()) - .WithChapter(new ChapterBuilder("3").WithCreated(now.AddMonths(2)).WithPages(1).Build()) - .WithChapter(new ChapterBuilder("4").WithCreated(now.AddMonths(3)).WithPages(1).Build()) - .Build()) - .Build()) - .Build()); - - - await Context.SaveChangesAsync(); - - var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); - Assert.NotNull(nextChapter); - Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); - Assert.Equal(5, nextChapter.ChapterNumber); - Assert.NotNull(nextChapter.ExpectedDate); - - var expected = now.AddMonths(4); - Assert.Equal(expected.Month, nextChapter.ExpectedDate.Value.Month); - Assert.True(nextChapter.ExpectedDate.Value.Day >= expected.Day - 1 || nextChapter.ExpectedDate.Value.Day <= expected.Day + 1); - } - - #endregion - } diff --git a/API.Tests/Services/SettingsServiceTests.cs b/API.Tests/Services/SettingsServiceTests.cs deleted file mode 100644 index a3c6b67b8..000000000 --- a/API.Tests/Services/SettingsServiceTests.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System.Collections.Generic; -using System.IO.Abstractions; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Services; -using API.Services.Tasks.Scanner; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class SettingsServiceTests -{ - private readonly ISettingsService _settingsService; - private readonly IUnitOfWork _mockUnitOfWork; - - public SettingsServiceTests() - { - var ds = new DirectoryService(Substitute.For>(), new FileSystem()); - - _mockUnitOfWork = Substitute.For(); - _settingsService = new SettingsService(_mockUnitOfWork, ds, - Substitute.For(), Substitute.For(), - Substitute.For>()); - } - - #region UpdateMetadataSettings - - [Fact] - public async Task UpdateMetadataSettings_ShouldUpdateExistingSettings() - { - // Arrange - var existingSettings = new MetadataSettings - { - Id = 1, - Enabled = false, - EnableSummary = false, - EnableLocalizedName = false, - EnablePublicationStatus = false, - EnableRelationships = false, - EnablePeople = false, - EnableStartDate = false, - EnableGenres = false, - EnableTags = false, - FirstLastPeopleNaming = false, - EnableCoverImage = false, - AgeRatingMappings = new Dictionary(), - Blacklist = [], - Whitelist = [], - Overrides = [], - PersonRoles = [], - FieldMappings = [] - }; - - var settingsRepo = Substitute.For(); - settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings)); - settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto())); - _mockUnitOfWork.SettingsRepository.Returns(settingsRepo); - - var updateDto = new MetadataSettingsDto - { - Enabled = true, - EnableSummary = true, - EnableLocalizedName = true, - EnablePublicationStatus = true, - EnableRelationships = true, - EnablePeople = true, - EnableStartDate = true, - EnableGenres = true, - EnableTags = true, - FirstLastPeopleNaming = true, - EnableCoverImage = true, - AgeRatingMappings = new Dictionary { { "Adult", AgeRating.R18Plus } }, - Blacklist = ["blacklisted-tag"], - Whitelist = ["whitelisted-tag"], - Overrides = [MetadataSettingField.Summary], - PersonRoles = [PersonRole.Writer], - FieldMappings = - [ - new MetadataFieldMappingDto - { - SourceType = MetadataFieldType.Genre, - DestinationType = MetadataFieldType.Tag, - SourceValue = "Action", - DestinationValue = "Fight", - ExcludeFromSource = true - } - ] - }; - - // Act - await _settingsService.UpdateMetadataSettings(updateDto); - - // Assert - await _mockUnitOfWork.Received(1).CommitAsync(); - - // Verify properties were updated - Assert.True(existingSettings.Enabled); - Assert.True(existingSettings.EnableSummary); - Assert.True(existingSettings.EnableLocalizedName); - Assert.True(existingSettings.EnablePublicationStatus); - Assert.True(existingSettings.EnableRelationships); - Assert.True(existingSettings.EnablePeople); - Assert.True(existingSettings.EnableStartDate); - Assert.True(existingSettings.EnableGenres); - Assert.True(existingSettings.EnableTags); - Assert.True(existingSettings.FirstLastPeopleNaming); - Assert.True(existingSettings.EnableCoverImage); - - // Verify collections were updated - Assert.Single(existingSettings.AgeRatingMappings); - Assert.Equal(AgeRating.R18Plus, existingSettings.AgeRatingMappings["Adult"]); - - Assert.Single(existingSettings.Blacklist); - Assert.Equal("blacklisted-tag", existingSettings.Blacklist[0]); - - Assert.Single(existingSettings.Whitelist); - Assert.Equal("whitelisted-tag", existingSettings.Whitelist[0]); - - Assert.Single(existingSettings.Overrides); - Assert.Equal(MetadataSettingField.Summary, existingSettings.Overrides[0]); - - Assert.Single(existingSettings.PersonRoles); - Assert.Equal(PersonRole.Writer, existingSettings.PersonRoles[0]); - - Assert.Single(existingSettings.FieldMappings); - Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].SourceType); - Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].DestinationType); - Assert.Equal("Action", existingSettings.FieldMappings[0].SourceValue); - Assert.Equal("Fight", existingSettings.FieldMappings[0].DestinationValue); - Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource); - } - - [Fact] - public async Task UpdateMetadataSettings_WithNullCollections_ShouldUseEmptyCollections() - { - // Arrange - var existingSettings = new MetadataSettings - { - Id = 1, - FieldMappings = [new MetadataFieldMapping {Id = 1, SourceValue = "OldValue"}] - }; - - var settingsRepo = Substitute.For(); - settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings)); - settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto())); - _mockUnitOfWork.SettingsRepository.Returns(settingsRepo); - - var updateDto = new MetadataSettingsDto - { - AgeRatingMappings = null, - Blacklist = null, - Whitelist = null, - Overrides = null, - PersonRoles = null, - FieldMappings = null - }; - - // Act - await _settingsService.UpdateMetadataSettings(updateDto); - - // Assert - await _mockUnitOfWork.Received(1).CommitAsync(); - - Assert.Empty(existingSettings.AgeRatingMappings); - Assert.Empty(existingSettings.Blacklist); - Assert.Empty(existingSettings.Whitelist); - Assert.Empty(existingSettings.Overrides); - Assert.Empty(existingSettings.PersonRoles); - - // Verify existing field mappings were cleared - settingsRepo.Received(1).RemoveRange(Arg.Any>()); - Assert.Empty(existingSettings.FieldMappings); - } - - [Fact] - public async Task UpdateMetadataSettings_WithFieldMappings_ShouldReplaceExistingMappings() - { - // Arrange - var existingSettings = new MetadataSettings - { - Id = 1, - FieldMappings = - [ - new MetadataFieldMapping - { - Id = 1, - SourceType = MetadataFieldType.Genre, - DestinationType = MetadataFieldType.Genre, - SourceValue = "OldValue", - DestinationValue = "OldDestination", - ExcludeFromSource = false - } - ] - }; - - var settingsRepo = Substitute.For(); - settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings)); - settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto())); - _mockUnitOfWork.SettingsRepository.Returns(settingsRepo); - - var updateDto = new MetadataSettingsDto - { - FieldMappings = - [ - new MetadataFieldMappingDto - { - SourceType = MetadataFieldType.Tag, - DestinationType = MetadataFieldType.Genre, - SourceValue = "NewValue", - DestinationValue = "NewDestination", - ExcludeFromSource = true - }, - - new MetadataFieldMappingDto - { - SourceType = MetadataFieldType.Tag, - DestinationType = MetadataFieldType.Tag, - SourceValue = "AnotherValue", - DestinationValue = "AnotherDestination", - ExcludeFromSource = false - } - ] - }; - - // Act - await _settingsService.UpdateMetadataSettings(updateDto); - - // Assert - await _mockUnitOfWork.Received(1).CommitAsync(); - - // Verify existing field mappings were cleared and new ones added - settingsRepo.Received(1).RemoveRange(Arg.Any>()); - Assert.Equal(2, existingSettings.FieldMappings.Count); - - // Verify first mapping - Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].SourceType); - Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].DestinationType); - Assert.Equal("NewValue", existingSettings.FieldMappings[0].SourceValue); - Assert.Equal("NewDestination", existingSettings.FieldMappings[0].DestinationValue); - Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource); - - // Verify second mapping - Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].SourceType); - Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].DestinationType); - Assert.Equal("AnotherValue", existingSettings.FieldMappings[1].SourceValue); - Assert.Equal("AnotherDestination", existingSettings.FieldMappings[1].DestinationValue); - Assert.False(existingSettings.FieldMappings[1].ExcludeFromSource); - } - - [Fact] - public async Task UpdateMetadataSettings_WithBlacklistWhitelist_ShouldNormalizeAndDeduplicateEntries() - { - // Arrange - var existingSettings = new MetadataSettings - { - Id = 1, - Blacklist = [], - Whitelist = [] - }; - - // We need to mock the repository and provide a custom implementation for ToNormalized - var settingsRepo = Substitute.For(); - settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings)); - settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto())); - _mockUnitOfWork.SettingsRepository.Returns(settingsRepo); - - var updateDto = new MetadataSettingsDto - { - // Include duplicates with different casing and whitespace - Blacklist = ["tag1", "Tag1", " tag2 ", "", " ", "tag3"], - Whitelist = ["allowed1", "Allowed1", " allowed2 ", "", "allowed3"] - }; - - // Act - await _settingsService.UpdateMetadataSettings(updateDto); - - // Assert - await _mockUnitOfWork.Received(1).CommitAsync(); - - Assert.Equal(3, existingSettings.Blacklist.Count); - Assert.Equal(3, existingSettings.Whitelist.Count); - } - - #endregion -} diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs index 3893af1fb..8bf32a0c1 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/API.Tests/Services/SiteThemeServiceTests.cs @@ -9,7 +9,6 @@ using API.Services; using API.Services.Tasks; using API.SignalR; using Kavita.Common; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -24,68 +23,123 @@ public abstract class SiteThemeServiceTest : AbstractDbTest private readonly IEventHub _messageHub = Substitute.For(); - protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) + protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) : base() { _testOutputHelper = testOutputHelper; } protected override async Task ResetDb() { - Context.SiteTheme.RemoveRange(Context.SiteTheme); - await Context.SaveChangesAsync(); + _context.SiteTheme.RemoveRange(_context.SiteTheme); + await _context.SaveChangesAsync(); // Recreate defaults - await Seed.SeedThemes(Context); + await Seed.SeedThemes(_context); } [Fact] public async Task UpdateDefault_ShouldThrowOnInvalidId() { await ResetDb(); - _testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), - Substitute.For>(), Substitute.For()); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); - Context.SiteTheme.Add(new SiteTheme() + _context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), - Provider = ThemeProvider.Custom, + Provider = ThemeProvider.User, FileName = "custom.css", IsDefault = false }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var ex = await Assert.ThrowsAsync(() => siteThemeService.UpdateDefault(10)); + var ex = await Assert.ThrowsAsync(async () => await siteThemeService.UpdateDefault(10)); Assert.Equal("Theme file missing or invalid", ex.Message); } + [Fact] + public async Task Scan_ShouldFindCustomFile() + { + await ResetDb(); + _testOutputHelper.WriteLine($"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); + await siteThemeService.Scan(); + + Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom")); + } + + [Fact] + public async Task Scan_ShouldOnlyInsertOnceOnSecondScan() + { + await ResetDb(); + _testOutputHelper.WriteLine( + $"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); + await siteThemeService.Scan(); + + Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom")); + + await siteThemeService.Scan(); + + var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t => + t.Name.ToNormalized().Equals("custom".ToNormalized())); + + Assert.Single(customThemes); + } + + [Fact] + public async Task Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan() + { + await ResetDb(); + _testOutputHelper.WriteLine($"[Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); + await siteThemeService.Scan(); + + Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom")); + + filesystem.RemoveFile($"{SiteThemeDirectory}custom.css"); + await siteThemeService.Scan(); + + var themes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()); + + Assert.Equal(0, themes.Count(t => + t.Name.ToNormalized().Equals("custom".ToNormalized()))); + } [Fact] public async Task GetContent_ShouldReturnContent() { await ResetDb(); - _testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), - Substitute.For>(), Substitute.For()); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); - Context.SiteTheme.Add(new SiteTheme() + _context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), - Provider = ThemeProvider.Custom, + Provider = ThemeProvider.User, FileName = "custom.css", IsDefault = false }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var content = await siteThemeService.GetContent((await UnitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")).Id); + var content = await siteThemeService.GetContent((await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")).Id); Assert.NotNull(content); Assert.NotEmpty(content); Assert.Equal("123", content); @@ -95,31 +149,30 @@ public abstract class SiteThemeServiceTest : AbstractDbTest public async Task UpdateDefault_ShouldHaveOneDefault() { await ResetDb(); - _testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), - Substitute.For>(), Substitute.For()); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); - Context.SiteTheme.Add(new SiteTheme() + _context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), - Provider = ThemeProvider.Custom, + Provider = ThemeProvider.User, FileName = "custom.css", IsDefault = false }); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); - var customTheme = (await UnitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")); + var customTheme = (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")); Assert.NotNull(customTheme); await siteThemeService.UpdateDefault(customTheme.Id); - Assert.Equal(customTheme.Id, (await UnitOfWork.SiteThemeRepository.GetDefaultTheme()).Id); + Assert.Equal(customTheme.Id, (await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Id); } } diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index 17e26139c..c94ff1c48 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -1,5 +1,7 @@ -using API.Helpers.Builders; +using API.Extensions; +using API.Helpers.Builders; using API.Services.Plus; +using API.Services.Tasks; namespace API.Tests.Services; using System.Collections.Generic; @@ -14,6 +16,7 @@ using API.Entities.Enums; using API.Helpers; using API.Services; using SignalR; +using Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -49,7 +52,7 @@ public class TachiyomiServiceTests Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); - _tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For>(), _readerService); + _tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For>(), _readerService); } @@ -122,12 +125,12 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -167,12 +170,12 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -218,7 +221,7 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -262,19 +265,18 @@ public class TachiyomiServiceTests Assert.Equal("21", latestChapter.Number); } - [Fact] public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress() { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) @@ -321,16 +323,13 @@ public class TachiyomiServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithPages(199).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(199).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithPages(192).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(192).Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithPages(255).Build()) + .WithChapter(new ChapterBuilder("0").WithPages(255).Build()) .Build()) .WithPages(646) .Build(); @@ -369,7 +368,7 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -422,12 +421,12 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -465,12 +464,12 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -515,7 +514,7 @@ public class TachiyomiServiceTests await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -563,12 +562,12 @@ public class TachiyomiServiceTests { await ResetDb(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) diff --git a/API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf b/API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf deleted file mode 100644 index 9fe4811a7..000000000 Binary files a/API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf and /dev/null differ diff --git a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf b/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf deleted file mode 100644 index 0e0ffa8c7..000000000 Binary files a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf and /dev/null differ diff --git a/API.Tests/Services/Test Data/BookService/encrypted.pdf b/API.Tests/Services/Test Data/BookService/encrypted.pdf deleted file mode 100644 index 64249b728..000000000 Binary files a/API.Tests/Services/Test Data/BookService/encrypted.pdf and /dev/null differ diff --git a/API.Tests/Services/Test Data/BookService/indirect.pdf b/API.Tests/Services/Test Data/BookService/indirect.pdf deleted file mode 100644 index 11ecdcb76..000000000 Binary files a/API.Tests/Services/Test Data/BookService/indirect.pdf and /dev/null differ diff --git a/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp b/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp deleted file mode 100644 index 0b46b66d2..000000000 Binary files a/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp and /dev/null differ diff --git a/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp b/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp deleted file mode 100644 index 475824863..000000000 Binary files a/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png deleted file mode 100644 index 8a386b2b8..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg b/API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg deleted file mode 100644 index ed53f6649..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png deleted file mode 100644 index 5b3d45386..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/green.png deleted file mode 100644 index 8ed6c4fe4..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/green.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png deleted file mode 100644 index 68b71ce39..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png deleted file mode 100644 index 9569f80d4..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png deleted file mode 100644 index a62988963..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png b/API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png deleted file mode 100644 index 0a4f36f30..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg b/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg deleted file mode 100644 index b185d6e41..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg b/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg deleted file mode 100644 index 99aafb10a..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal.jpg b/API.Tests/Services/Test Data/ImageService/Covers/comic-normal.jpg deleted file mode 100644 index 91a8f9b8e..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg b/API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg deleted file mode 100644 index 6ee3931b3..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg b/API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg deleted file mode 100644 index 3442c8b32..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png b/API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png deleted file mode 100644 index eae5138c6..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/spread-cover.jpg b/API.Tests/Services/Test Data/ImageService/Covers/spread-cover.jpg deleted file mode 100644 index 449400181..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/spread-cover.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip-2.png b/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip-2.png deleted file mode 100644 index e89641384..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip-2.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip.jpg b/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip.jpg deleted file mode 100644 index 469cb9bc3..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip.jpg and /dev/null differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/wide-ad.png b/API.Tests/Services/Test Data/ImageService/Covers/wide-ad.png deleted file mode 100644 index 2ad5103fe..000000000 Binary files a/API.Tests/Services/Test Data/ImageService/Covers/wide-ad.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ReadingListService/Annual.cbl b/API.Tests/Services/Test Data/ReadingListService/Annual.cbl deleted file mode 100644 index a6dd3167e..000000000 --- a/API.Tests/Services/Test Data/ReadingListService/Annual.cbl +++ /dev/null @@ -1,19 +0,0 @@ - - - Fables - - - 5bd3dd55-2a85-4325-aefa-21e9f19b12c9 - - - 3831761c-604a-4420-bed2-9f5ac4e94bd4 - - - 23acefd4-1bc7-4c3c-99df-133045d1f266 - - - 27a5d7db-9f7e-4be1-aca6-998a1cc1488f - - - - diff --git a/API.Tests/Services/Test Data/ScannerService/1x1.png b/API.Tests/Services/Test Data/ScannerService/1x1.png deleted file mode 100644 index 94381b429..000000000 Binary files a/API.Tests/Services/Test Data/ScannerService/1x1.png and /dev/null differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Books/PDFs/Rollo at Work SP01.pdf b/API.Tests/Services/Test Data/ScannerService/Library/Books/PDFs/Rollo at Work SP01.pdf new file mode 100644 index 000000000..35983f4e0 Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Books/PDFs/Rollo at Work SP01.pdf differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Books/The Golden Harpoon/The Golden Harpoon.epub b/API.Tests/Services/Test Data/ScannerService/Library/Books/The Golden Harpoon/The Golden Harpoon.epub new file mode 100644 index 000000000..7388bc85e Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Books/The Golden Harpoon/The Golden Harpoon.epub differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Books/Vertical Reading/01.epub b/API.Tests/Services/Test Data/ScannerService/Library/Books/Vertical Reading/01.epub new file mode 100644 index 000000000..2850eed96 Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Books/Vertical Reading/01.epub differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 1.cbz b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 1.cbz new file mode 100644 index 000000000..d1eb76880 Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 1.cbz differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 2.zip b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 2.zip new file mode 100644 index 000000000..40ebeb13e Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 1 Chapter 2.zip differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 2 Chapter 3.zip b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 2 Chapter 3.zip new file mode 100644 index 000000000..40ebeb13e Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/Accel World Vol 2 Chapter 3.zip differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/ComicInfo.xml b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/ComicInfo.xml new file mode 100644 index 000000000..6bc41f434 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Accel World/ComicInfo.xml @@ -0,0 +1,5 @@ + + + Accel World + 2 + \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/ComicInfo.xml b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/ComicInfo.xml new file mode 100644 index 000000000..d0494448f --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/ComicInfo.xml @@ -0,0 +1,6 @@ + + + Hajime no Ippo + 3 + M + \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz new file mode 100644 index 000000000..895cfc415 Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 2.zip b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 2.zip new file mode 100644 index 000000000..40ebeb13e Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 2.zip differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 3.zip b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 3.zip new file mode 100644 index 000000000..40ebeb13e Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 3.zip differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/001.jpg b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/001.jpg new file mode 100644 index 000000000..b5c6de2aa Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/001.jpg differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/002.jpg b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/002.jpg new file mode 100644 index 000000000..b5c6de2aa Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/002.jpg differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/003.jpg b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/003.jpg new file mode 100644 index 000000000..b5c6de2aa Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/003.jpg differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/004.jpg b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/004.jpg new file mode 100644 index 000000000..b5c6de2aa Binary files /dev/null and b/API.Tests/Services/Test Data/ScannerService/Library/Manga/Pumpkin Night (images)/Chapter 01/004.jpg differ diff --git a/API.Tests/Services/Test Data/ScannerService/Library/README.md b/API.Tests/Services/Test Data/ScannerService/Library/README.md new file mode 100644 index 000000000..2969111b4 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/Library/README.md @@ -0,0 +1 @@ +This is an example of a layout. All files in here have non-copyrighted data but emulate real series to ensure the Process series Works as expected. \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_omake.zip similarity index 100% rename from UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_omake.zip diff --git a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v01.zip similarity index 100% rename from UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v01.zip diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v02.zip similarity index 100% rename from UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v02.zip diff --git a/UI/Web/src/app/_single-module/related-tab/related-tab.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v03.zip similarity index 100% rename from UI/Web/src/app/_single-module/related-tab/related-tab.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v03.zip diff --git a/UI/Web/src/app/_single-module/review-modal/review-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v04.zip similarity index 100% rename from UI/Web/src/app/_single-module/review-modal/review-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v04.zip diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v01 (digital).cbz similarity index 100% rename from UI/Web/src/app/_single-module/reviews/reviews.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v01 (digital).cbz diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v02.cbz similarity index 100% rename from UI/Web/src/app/_single-module/sort-button/sort-button.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v02.cbz diff --git a/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v03.cbz similarity index 100% rename from UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v03.cbz diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v04.cbz similarity index 100% rename from UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v04.cbz diff --git a/UI/Web/src/app/admin/manage-scrobling/manage-scrobbling.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v05.cbz similarity index 100% rename from UI/Web/src/app/admin/manage-scrobling/manage-scrobbling.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v05.cbz diff --git a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v06.cbz similarity index 100% rename from UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v06.cbz diff --git a/UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v07.cbz similarity index 100% rename from UI/Web/src/app/announcements/_components/new-update-modal/new-update-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v07.cbz diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v08.cbz similarity index 100% rename from UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v08.cbz diff --git a/UI/Web/src/app/cards/edit-chapter-progress/edit-chapter-progress.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v09.cbz similarity index 100% rename from UI/Web/src/app/cards/edit-chapter-progress/edit-chapter-progress.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v09.cbz diff --git a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v10.cbz similarity index 100% rename from UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v10.cbz diff --git a/UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v11.cbz similarity index 100% rename from UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v11.cbz diff --git a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v12.cbz similarity index 100% rename from UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v12.cbz diff --git a/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v13.cbz similarity index 100% rename from UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v13.cbz diff --git a/UI/Web/src/app/series-detail/_components/download-button/download-button.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v14.cbz similarity index 100% rename from UI/Web/src/app/series-detail/_components/download-button/download-button.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v14.cbz diff --git a/UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v15.cbz similarity index 100% rename from UI/Web/src/app/series-detail/_components/rating-modal/rating-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v15.cbz diff --git a/UI/Web/src/app/shared/_components/carousel-modal/preview-image-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v01.cbz similarity index 100% rename from UI/Web/src/app/shared/_components/carousel-modal/preview-image-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v01.cbz diff --git a/UI/Web/src/app/shared/_components/promoted-icon/promoted-icon.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v02.cbz similarity index 100% rename from UI/Web/src/app/shared/_components/promoted-icon/promoted-icon.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v02.cbz diff --git a/UI/Web/src/app/shared/edit-list/edit-list.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v03.cbz similarity index 100% rename from UI/Web/src/app/shared/edit-list/edit-list.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v03.cbz diff --git a/UI/Web/src/app/sidenav/_components/edit-smart-filter-modal/edit-smart-filter-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v04.cbz similarity index 100% rename from UI/Web/src/app/sidenav/_components/edit-smart-filter-modal/edit-smart-filter-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v04.cbz diff --git a/UI/Web/src/app/sidenav/_components/manage-customization/manage-customization.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v05.cbz similarity index 100% rename from UI/Web/src/app/sidenav/_components/manage-customization/manage-customization.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v05.cbz diff --git a/UI/Web/src/app/statistics/_components/_modals/generic-table-modal/generic-table-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v06.cbz similarity index 100% rename from UI/Web/src/app/statistics/_components/_modals/generic-table-modal/generic-table-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v06.cbz diff --git a/UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v07.cbz similarity index 100% rename from UI/Web/src/app/user-settings/_modals/edit-device-modal/edit-device-modal.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v07.cbz diff --git a/UI/Web/src/app/user-settings/manage-opds/manage-opds.component.scss b/API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v10.cbz similarity index 100% rename from UI/Web/src/app/user-settings/manage-opds/manage-opds.component.scss rename to API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v10.cbz diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json deleted file mode 100644 index 791dcdc44..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Root 1/Antarctic Press/Plush/Plush v01.cbz", - "Root 1/Antarctic Press/Plush/Plush v02.cbz", - "Root 2/Accel/Accel v01.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json deleted file mode 100644 index 791dcdc44..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Root 1/Antarctic Press/Plush/Plush v01.cbz", - "Root 1/Antarctic Press/Plush/Plush v02.cbz", - "Root 2/Accel/Accel v01.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json deleted file mode 100644 index fe931174e..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Antarctic Press/Plush/Plush v01.cbz", - "Antarctic Press/Plush/Plush v02.cbz", - "Antarctic Press/Plush/Extra/Plush v03.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json deleted file mode 100644 index 6b4b70160..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "My Dress-Up Darling/My Dress-Up Darling v01.cbz", - "My Dress-Up Darling/My Dress-Up Darling v02.cbz", - "My Dress-Up Darling/My Dress-Up Darling ch 10.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json deleted file mode 100644 index 12e80ea95..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "My Dress-Up Darling/My Dress-Up Darling v01.cbz", - "My Dress-Up Darling/My Dress-Up Darling v02.cbz", - "My Dress-Up Darling/My Dress-Up Darling ch 10.cbz", - "My Dress-Up Darling/Specials/Official Anime Fanbook SP05 (2024) (Digital).cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json deleted file mode 100644 index 06ed0f1f9..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "My Dress-Up Darling/My Dress-Up Darling v01.cbz", - "My Dress-Up Darling/My Dress-Up Darling v02.cbz", - "My Dress-Up Darling/My Dress-Up Darling ch 10.cbz", - "My Dress-Up Darling/Specials/My Dress-Up Darling - Omakes SP01.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json deleted file mode 100644 index 3fa9eebf7..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json deleted file mode 100644 index d283a3460..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "葬送のフィリーレン/葬送のフィリーレン vol 1/0001.png", - "葬送のフィリーレン/葬送のフィリーレン vol 2/0002.png", - "葬送のフィリーレン/Specials/葬送のフリーレン 公式ファンブック SP01/0001.png" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json deleted file mode 100644 index 62106703c..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "My Dress-Up Darling/My Dress-Up Darling vol 1/0001.png", - "My Dress-Up Darling/My Dress-Up Darling vol 1/0002.png", - "My Dress-Up Darling/My Dress-Up Darling vol 2/0001.png", - "My Dress-Up Darling/Specials/My Dress-Up Darling SP01/0001.png" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json deleted file mode 100644 index feb1fd99f..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "Immoral Guild/Futoku no Guild v01.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json deleted file mode 100644 index c9d4b14b6..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Root 1/Antarctic Press/Plush/Plush v01.cbz", - "Root 1/Antarctic Press/Plush/Plush v02.cbz", - "Root 2/Accel/Accel v01.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json deleted file mode 100644 index 586ae90f5..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "My Dress-Up Darling/Chapter 1/01.cbz", - "My Dress-Up Darling/Chapter 2/02.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json b/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json deleted file mode 100644 index f5097b369..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "Dreadwolf/Dreadwolf Chapter 1-12.pdf", - "Dreadwolf/Dreadwolf Chapter 13-24.pdf", - "Dreadwolf/Dreadwolf Chapter 25.pdf", - "Dreadwolf/Dreadwolf Chapter 26.pdf" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json b/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json deleted file mode 100644 index f5097b369..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "Dreadwolf/Dreadwolf Chapter 1-12.pdf", - "Dreadwolf/Dreadwolf Chapter 13-24.pdf", - "Dreadwolf/Dreadwolf Chapter 25.pdf", - "Dreadwolf/Dreadwolf Chapter 26.pdf" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Publisher - ComicVine.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Publisher - ComicVine.json deleted file mode 100644 index f73beffff..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Publisher - ComicVine.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - "Antarctic Press/Plush (2018)/Plush 002 (2019).cbz", - "Antarctic Press/Plush (2018)/Plush 001 (2018).cbz", - "12-Gauge Comics/Plush (2022)/Plush 1 (2022).cbz", - "12-Gauge Comics/Plush (2022)/Plush 2 (2022).cbz", - "12-Gauge Comics/Plush (2022)/Plush 3 (2023).cbz", - "12-Gauge Comics/Plush (2022)/Plush 004 (2023).cbz", - "12-Gauge Comics/Plush (2022)/Plush 005 (2023).cbz", - "12-Gauge Comics/Plush (2022)/Plush 006 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 009 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 1 (2022).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 2 (2022).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 3 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 004 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 005 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 006 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 007 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 008 (2023).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 010 (2024).cbz", - "Ablaze/Traveling to Mars (2022)/Traveling to Mars 011 (2024).cbz", - "Blood Hunters V2024 (2024)/Blood Hunters 001 (2024).cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json deleted file mode 100644 index 803f92586..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE/[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE.zip", - "[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE/[218565]-(C92) [BRIO (Puyocha)] Something Else (THE IDOLM@STE.zip" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json deleted file mode 100644 index 410994952..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "Dress Up Darling/Dress Up Darling Ch 01.cbz", - "Dress Up Darling/Dress Up Darling/Dress Up Darling Vol 01.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json deleted file mode 100644 index c6ea3bc88..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "Spice and Wolf/Spice and Wolf Vol. 1.cbz", - "Spice and Wolf/Spice and Wolf Vol. 2.cbz", - "The Executioner and Her Way of Life/The Executioner and Her Way of Life Vol. 1.cbz", - "The Executioner and Her Way of Life/The Executioner and Her Way of Life Vol. 2.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json deleted file mode 100644 index 7ddcaecf4..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "The Novel's Extra (Remake)/Vol.01.cbz", - "The Novel's Extra (Remake)/The Novel's Extra Chapter 100.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json deleted file mode 100644 index 6495c294f..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "My Dress-Up Darling/My Dress-Up Darling v01.cbz", - "My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru v02.cbz", - "My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 10.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json deleted file mode 100644 index 26619df88..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Immoral Guild/Immoral Guild v01.cbz", - "Immoral Guild/Immoral Guild v02.cbz", - "Immoral Guild/Futoku No Guild - Vol. 12 Ch. 67 - Take Responsibility.cbz" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json deleted file mode 100644 index d6e91183b..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Immoral Guild/Immoral Guild v01.cbz", - "Immoral Guild/Immoral Guild v02.cbz", - "Immoral Guild/Futoku No Guild - Vol. 12 Ch. 67 - Take Responsibility.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json deleted file mode 100644 index fc2bee18c..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "The Avengers/The Avengers vol 1.pdf" -] \ No newline at end of file diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json deleted file mode 100644 index 0b2dd765d..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 1-3.cbz", - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 4.cbz", - "Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 5.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json deleted file mode 100644 index 4574ddb4e..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - "VizMedia/Frieren - Beyond Journey's End/Frieren - Beyond Journey's End Vol. 1.cbz", - "VizMedia/Frieren - Beyond Journey's End/Frieren - Beyond Journey's End Vol. 2.cbz", - "VizMedia/Seraph of the End/Seraph of the End Vol. 1.cbz", - "YenPress/Spice and Wolf/Spice and Wolf Vol. 1.cbz", - "YenPress/Spice and Wolf/Spice and Wolf Vol. 2.cbz", - "YenPress/The Executioner and Her Way of Life/The Executioner and Her Way of Life Vol. 1.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json deleted file mode 100644 index 3d7c74d5c..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - "Frieren - Beyond Journey's End/Frieren - Beyond Journey's End Vol. 1/Frieren - Beyond Journey's End Ch. 0001.cbz", - "Frieren - Beyond Journey's End/Frieren - Beyond Journey's End Vol. 1/Frieren - Beyond Journey's End Ch. 0002.cbz", - "Seraph of the End/Seraph of the End Vol. 1/Seraph of the End Ch. 0001.cbz", - "Spice and Wolf/Spice and Wolf Vol. 1/Spice and Wolf Vol. 1 Ch. 0001.cbz", - "Spice and Wolf/Spice and Wolf Vol. 1/Spice and Wolf Vol. 1 Ch. 0002.cbz", - "Spice and Wolf/Spice and Wolf Vol. 2/Spice and Wolf Vol. 2 Ch. 0003.cbz", - "The Executioner and Her Way of Life/The Executioner and Her Way of Life Vol. 1/The Executioner and Her Way of Life Vol. 1 Ch. 0001.cbz", - "The Executioner and Her Way of Life/The Executioner and Her Way of Life Vol. 2/The Executioner and Her Way of Life Vol. 2 Ch. 0003.cbz" -] - diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json deleted file mode 100644 index 103ea421a..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "Spice and Wolf/Spice and Wolf Vol. 1.cbz", - "Spice and Wolf/Spice and Wolf Vol. 2.cbz", - "Spice and Wolf/Spice and Wolf Vol. 3/Spice and Wolf Vol. 3 Ch. 0011.cbz", - "Spice and Wolf/Spice and Wolf Vol. 3/Spice and Wolf Vol. 3 Ch. 0012.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json deleted file mode 100644 index 103ea421a..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "Spice and Wolf/Spice and Wolf Vol. 1.cbz", - "Spice and Wolf/Spice and Wolf Vol. 2.cbz", - "Spice and Wolf/Spice and Wolf Vol. 3/Spice and Wolf Vol. 3 Ch. 0011.cbz", - "Spice and Wolf/Spice and Wolf Vol. 3/Spice and Wolf Vol. 3 Ch. 0012.cbz" -] diff --git a/API.Tests/Services/TokenServiceTests.cs b/API.Tests/Services/TokenServiceTests.cs deleted file mode 100644 index d40be5bc9..000000000 --- a/API.Tests/Services/TokenServiceTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using API.Services; -using Xunit; - -namespace API.Tests.Services; - -public class TokenServiceTests -{ - [Fact] - public void HasTokenExpired_OldToken() - { - // ValidTo: 1/1/1990 - var result = TokenService.HasTokenExpired("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjYzMzgzMDM5OX0.KM_cUKSaCJL3ts0Qim3ZHUeJT7yf-wKoLdKb0rx0VbU"); - - Assert.True(result); - } - - [Fact] - public void HasTokenExpired_ValidInFuture() - { - // ValidTo: 4/11/2200 - var result = TokenService.HasTokenExpired("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjcyNjg0ODYzOTl9.nZrN5USbUmMYDKwkPoMtEAhTeYTeaikgAeSzDPj5kZQ"); - Assert.False(result); - } - - [Fact] - public void HasTokenExpired_NoToken() - { - var result = TokenService.HasTokenExpired(""); - Assert.True(result); - } -} diff --git a/API.Tests/Services/VersionUpdaterServiceTests.cs b/API.Tests/Services/VersionUpdaterServiceTests.cs deleted file mode 100644 index 8be8f4aee..000000000 --- a/API.Tests/Services/VersionUpdaterServiceTests.cs +++ /dev/null @@ -1,446 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using API.DTOs.Update; -using API.Services; -using API.Services.Tasks; -using API.SignalR; -using Flurl.Http.Testing; -using Kavita.Common.EnvironmentInfo; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class VersionUpdaterServiceTests : IDisposable -{ - private readonly ILogger _logger = Substitute.For>(); - private readonly IEventHub _eventHub = Substitute.For(); - private readonly IDirectoryService _directoryService = Substitute.For(); - private readonly VersionUpdaterService _service; - private readonly string _tempPath; - private readonly HttpTest _httpTest; - - public VersionUpdaterServiceTests() - { - // Create temp directory for cache - _tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_tempPath); - _directoryService.LongTermCacheDirectory.Returns(_tempPath); - - _service = new VersionUpdaterService(_logger, _eventHub, _directoryService); - - // Setup HTTP testing - _httpTest = new HttpTest(); - - // Mock BuildInfo.Version for consistent testing - typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, new Version("0.5.0.0")); - } - - public void Dispose() - { - _httpTest.Dispose(); - - // Cleanup temp directory - if (Directory.Exists(_tempPath)) - { - Directory.Delete(_tempPath, true); - } - - // Reset BuildInfo.Version - typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, null); - GC.SuppressFinalize(this); - } - - [Fact] - public async Task CheckForUpdate_ShouldReturnNull_WhenGithubApiReturnsNull() - { - - _httpTest.RespondWith("null"); - - - var result = await _service.CheckForUpdate(); - - - Assert.Null(result); - } - - // Depends on BuildInfo.CurrentVersion - //[Fact] - public async Task CheckForUpdate_ShouldReturnUpdateNotification_WhenNewVersionIsAvailable() - { - - var githubResponse = new - { - tag_name = "v0.6.0", - name = "Release 0.6.0", - body = "# Added\n- Feature 1\n- Feature 2\n# Fixed\n- Bug 1\n- Bug 2", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.6.0", - published_at = DateTime.UtcNow.ToString("o") - }; - - _httpTest.RespondWithJson(githubResponse); - - - var result = await _service.CheckForUpdate(); - - - Assert.NotNull(result); - Assert.Equal("0.6.0", result.UpdateVersion); - Assert.Equal("0.5.0.0", result.CurrentVersion); - Assert.True(result.IsReleaseNewer); - Assert.Equal(2, result.Added.Count); - Assert.Equal(2, result.Fixed.Count); - } - - //[Fact] - public async Task CheckForUpdate_ShouldDetectEqualVersion() - { - // I can't figure this out - typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, new Version("0.5.0.0")); - - - var githubResponse = new - { - tag_name = "v0.5.0", - name = "Release 0.5.0", - body = "# Added\n- Feature 1", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.5.0", - published_at = DateTime.UtcNow.ToString("o") - }; - - _httpTest.RespondWithJson(githubResponse); - - - var result = await _service.CheckForUpdate(); - - - Assert.NotNull(result); - Assert.True(result.IsReleaseEqual); - Assert.False(result.IsReleaseNewer); - } - - - //[Fact] - public async Task PushUpdate_ShouldSendUpdateEvent_WhenNewerVersionAvailable() - { - - var update = new UpdateNotificationDto - { - UpdateVersion = "0.6.0", - CurrentVersion = "0.5.0.0", - UpdateBody = "", - UpdateTitle = null, - UpdateUrl = null, - PublishDate = null - }; - - - await _service.PushUpdate(update); - - - await _eventHub.Received(1).SendMessageAsync( - Arg.Is(MessageFactory.UpdateAvailable), - Arg.Any(), - Arg.Is(true) - ); - } - - [Fact] - public async Task PushUpdate_ShouldNotSendUpdateEvent_WhenVersionIsEqual() - { - - var update = new UpdateNotificationDto - { - UpdateVersion = "0.5.0.0", - CurrentVersion = "0.5.0.0", - UpdateBody = "", - UpdateTitle = null, - UpdateUrl = null, - PublishDate = null - }; - - - await _service.PushUpdate(update); - - - await _eventHub.DidNotReceive().SendMessageAsync( - Arg.Any(), - Arg.Any(), - Arg.Any() - ); - } - - [Fact] - public async Task GetAllReleases_ShouldReturnReleases_LimitedByCount() - { - - var releases = new List - { - new - { - tag_name = "v0.7.0", - name = "Release 0.7.0", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.0", - published_at = DateTime.UtcNow.AddDays(-1).ToString("o") - }, - new - { - tag_name = "v0.6.0", - name = "Release 0.6.0", - body = "# Added\n- Feature B", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.6.0", - published_at = DateTime.UtcNow.AddDays(-10).ToString("o") - }, - new - { - tag_name = "v0.5.0", - name = "Release 0.5.0", - body = "# Added\n- Feature C", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.5.0", - published_at = DateTime.UtcNow.AddDays(-20).ToString("o") - } - }; - - _httpTest.RespondWithJson(releases); - - - var result = await _service.GetAllReleases(2); - - - Assert.Equal(2, result.Count); - Assert.Equal("0.7.0.0", result[0].UpdateVersion); - Assert.Equal("0.6.0", result[1].UpdateVersion); - } - - [Fact] - public async Task GetAllReleases_ShouldUseCachedData_WhenCacheIsValid() - { - - var releases = new List - { - new() - { - UpdateVersion = "0.6.0", - CurrentVersion = "0.5.0.0", - PublishDate = DateTime.UtcNow.AddDays(-10) - .ToString("o"), - UpdateBody = "", - UpdateTitle = null, - UpdateUrl = null - } - }; - releases.Add(new() - { - UpdateVersion = "0.7.0", - CurrentVersion = "0.5.0.0", - PublishDate = DateTime.UtcNow.AddDays(-1) - .ToString("o"), - UpdateBody = "", - UpdateTitle = null, - UpdateUrl = null - }); - - // Create cache file - var cacheFilePath = Path.Combine(_tempPath, "github_releases_cache.json"); - await File.WriteAllTextAsync(cacheFilePath, System.Text.Json.JsonSerializer.Serialize(releases)); - File.SetLastWriteTimeUtc(cacheFilePath, DateTime.UtcNow); // Ensure it's fresh - - - var result = await _service.GetAllReleases(); - - - Assert.Equal(2, result.Count); - Assert.Empty(_httpTest.CallLog); // No HTTP calls made - } - - [Fact] - public async Task GetAllReleases_ShouldFetchNewData_WhenCacheIsExpired() - { - - var releases = new List - { - new() - { - UpdateVersion = "0.6.0", - CurrentVersion = "0.5.0.0", - PublishDate = DateTime.UtcNow.AddDays(-10) - .ToString("o"), - UpdateBody = null, - UpdateTitle = null, - UpdateUrl = null - } - }; - - // Create expired cache file - var cacheFilePath = Path.Combine(_tempPath, "github_releases_cache.json"); - await File.WriteAllTextAsync(cacheFilePath, System.Text.Json.JsonSerializer.Serialize(releases)); - File.SetLastWriteTimeUtc(cacheFilePath, DateTime.UtcNow.AddHours(-2)); // Expired (older than 1 hour) - - // Setup HTTP response for new fetch - var newReleases = new List - { - new - { - tag_name = "v0.7.0", - name = "Release 0.7.0", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.0", - published_at = DateTime.UtcNow.ToString("o") - } - }; - - _httpTest.RespondWithJson(newReleases); - - - var result = await _service.GetAllReleases(); - - - Assert.Single(result); - Assert.Equal("0.7.0.0", result[0].UpdateVersion); - Assert.NotEmpty(_httpTest.CallLog); // HTTP call was made - } - - public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount() - { - - var releases = new List - { - new - { - tag_name = "v0.7.0", - name = "Release 0.7.0", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.0", - published_at = DateTime.UtcNow.AddDays(-1).ToString("o") - }, - new - { - tag_name = "v0.6.0", - name = "Release 0.6.0", - body = "# Added\n- Feature B", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.6.0", - published_at = DateTime.UtcNow.AddDays(-10).ToString("o") - }, - new - { - tag_name = "v0.5.0", - name = "Release 0.5.0", - body = "# Added\n- Feature C", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.5.0", - published_at = DateTime.UtcNow.AddDays(-20).ToString("o") - } - }; - - _httpTest.RespondWithJson(releases); - - - var result = await _service.GetNumberOfReleasesBehind(); - - - Assert.Equal(2 + 1, result); // Behind 0.7.0 and 0.6.0 - We have to add 1 because the current release is > 0.7.0 - } - - public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount_WithNightlies() - { - - var releases = new List - { - new - { - tag_name = "v0.7.1", - name = "Release 0.7.1", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.1", - published_at = DateTime.UtcNow.AddDays(-1).ToString("o") - }, - new - { - tag_name = "v0.7.0", - name = "Release 0.7.0", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.0", - published_at = DateTime.UtcNow.AddDays(-10).ToString("o") - }, - }; - - _httpTest.RespondWithJson(releases); - - - var result = await _service.GetNumberOfReleasesBehind(); - - - Assert.Equal(2, result); // We have to add 1 because the current release is > 0.7.0 - } - - [Fact] - public async Task ParseReleaseBody_ShouldExtractSections() - { - - var githubResponse = new - { - tag_name = "v0.6.0", - name = "Release 0.6.0", - body = "This is a great release with many improvements!\n\n# Added\n- Feature 1\n- Feature 2\n# Fixed\n- Bug 1\n- Bug 2\n# Changed\n- Change 1\n# Developer\n- Dev note 1", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.6.0", - published_at = DateTime.UtcNow.ToString("o") - }; - - _httpTest.RespondWithJson(githubResponse); - - - var result = await _service.CheckForUpdate(); - - - Assert.NotNull(result); - Assert.Equal(2, result.Added.Count); - Assert.Equal(2, result.Fixed.Count); - Assert.Equal(1, result.Changed.Count); - Assert.Equal(1, result.Developer.Count); - Assert.Contains("This is a great release", result.BlogPart); - } - - [Fact] - public async Task GetAllReleases_ShouldHandleNightlyBuilds() - { - - // Set BuildInfo.Version to a nightly build version - typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, new Version("0.7.1.0")); - - // Mock regular releases - var releases = new List - { - new - { - tag_name = "v0.7.0", - name = "Release 0.7.0", - body = "# Added\n- Feature A", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.7.0", - published_at = DateTime.UtcNow.AddDays(-1).ToString("o") - }, - new - { - tag_name = "v0.6.0", - name = "Release 0.6.0", - body = "# Added\n- Feature B", - html_url = "https://github.com/Kareadita/Kavita/releases/tag/v0.6.0", - published_at = DateTime.UtcNow.AddDays(-10).ToString("o") - } - }; - - _httpTest.RespondWithJson(releases); - - // Mock commit info for develop branch - _httpTest.RespondWithJson(new List()); - - - var result = await _service.GetAllReleases(); - - - Assert.NotNull(result); - Assert.True(result[0].IsOnNightlyInRelease); - } -} diff --git a/API.Tests/Services/WordCountAnalysisTests.cs b/API.Tests/Services/WordCountAnalysisTests.cs index 57c6ec7f6..6d17f3834 100644 --- a/API.Tests/Services/WordCountAnalysisTests.cs +++ b/API.Tests/Services/WordCountAnalysisTests.cs @@ -1,17 +1,19 @@ using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Entities; using API.Entities.Enums; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; +using API.Services.Tasks; using API.Services.Tasks.Metadata; using API.SignalR; +using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -24,12 +26,11 @@ public class WordCountAnalysisTests : AbstractDbTest private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); private const long WordCount = 33608; // 37417 if splitting on space, 33608 if just character count private const long MinHoursToRead = 1; - private const float AvgHoursToRead = 1.66954792f; + private const long AvgHoursToRead = 2; private const long MaxHoursToRead = 3; - - public WordCountAnalysisTests() + public WordCountAnalysisTests() : base() { - _readerService = new ReaderService(UnitOfWork, Substitute.For>(), + _readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); @@ -37,9 +38,9 @@ public class WordCountAnalysisTests : AbstractDbTest protected override async Task ResetDb() { - Context.Series.RemoveRange(Context.Series.ToList()); + _context.Series.RemoveRange(_context.Series.ToList()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); } [Fact] @@ -57,34 +58,34 @@ public class WordCountAnalysisTests : AbstractDbTest MangaFormat.Epub).Build()) .Build(); - Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithSeries(series) .Build()); series.Volumes = new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder("0") .WithChapter(chapter) .Build(), }; - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var cacheService = new CacheHelper(new FileService()); - var service = new WordCountAnalyzerService(Substitute.For>(), UnitOfWork, - Substitute.For(), cacheService, _readerService, Substitute.For()); + var service = new WordCountAnalyzerService(Substitute.For>(), _unitOfWork, + Substitute.For(), cacheService, _readerService); await service.ScanSeries(1, 1); Assert.Equal(WordCount, series.WordCount); Assert.Equal(MinHoursToRead, series.MinHoursToRead); - Assert.True(series.AvgHoursToRead.Is(AvgHoursToRead)); + Assert.Equal(AvgHoursToRead, series.AvgHoursToRead); Assert.Equal(MaxHoursToRead, series.MaxHoursToRead); // Validate the Chapter gets updated correctly - var volume = series.Volumes[0]; + var volume = series.Volumes.First(); Assert.Equal(WordCount, volume.WordCount); Assert.Equal(MinHoursToRead, volume.MinHoursToRead); Assert.Equal(AvgHoursToRead, volume.AvgHoursToRead); @@ -110,22 +111,22 @@ public class WordCountAnalysisTests : AbstractDbTest .Build(); var series = new SeriesBuilder("Test Series") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder("0") .WithChapter(chapter) .Build()) .Build(); - Context.Library.Add(new LibraryBuilder("Test", LibraryType.Book) + _context.Library.Add(new LibraryBuilder("Test", LibraryType.Book) .WithSeries(series) .Build()); - await Context.SaveChangesAsync(); + await _context.SaveChangesAsync(); var cacheService = new CacheHelper(new FileService()); - var service = new WordCountAnalyzerService(Substitute.For>(), UnitOfWork, - Substitute.For(), cacheService, _readerService, Substitute.For()); + var service = new WordCountAnalyzerService(Substitute.For>(), _unitOfWork, + Substitute.For(), cacheService, _readerService); await service.ScanSeries(1, 1); var chapter2 = new ChapterBuilder("2") @@ -140,21 +141,23 @@ public class WordCountAnalysisTests : AbstractDbTest .WithChapter(chapter2) .Build()); - series.Volumes[0].Chapters.Add(chapter2); - await UnitOfWork.CommitAsync(); + series.Volumes.First().Chapters.Add(chapter2); + await _unitOfWork.CommitAsync(); await service.ScanSeries(1, 1); Assert.Equal(WordCount * 2L, series.WordCount); Assert.Equal(MinHoursToRead * 2, series.MinHoursToRead); + //Assert.Equal(AvgHoursToRead * 2, series.AvgHoursToRead); + //Assert.Equal((MaxHoursToRead * 2) - 1, series.MaxHoursToRead); // This is just a rounding issue - var firstVolume = series.Volumes[0]; + var firstVolume = series.Volumes.ElementAt(0); Assert.Equal(WordCount, firstVolume.WordCount); Assert.Equal(MinHoursToRead, firstVolume.MinHoursToRead); - Assert.True(series.AvgHoursToRead.Is(AvgHoursToRead * 2)); + Assert.Equal(AvgHoursToRead, firstVolume.AvgHoursToRead); Assert.Equal(MaxHoursToRead, firstVolume.MaxHoursToRead); - var secondVolume = series.Volumes[1]; + var secondVolume = series.Volumes.ElementAt(1); Assert.Equal(WordCount, secondVolume.WordCount); Assert.Equal(MinHoursToRead, secondVolume.MinHoursToRead); Assert.Equal(AvgHoursToRead, secondVolume.AvgHoursToRead); diff --git a/API/API.csproj b/API/API.csproj index a7d1177dc..3df9b220e 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -2,7 +2,7 @@ Default - net9.0 + net7.0 true Linux true @@ -12,6 +12,9 @@ latestmajor + + + false @@ -50,58 +53,57 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + @@ -111,16 +113,16 @@ + - - - + + + - @@ -138,7 +140,6 @@ - @@ -189,15 +190,7 @@ - - Always - - - Always - - - - + diff --git a/API/API.csproj.DotSettings b/API/API.csproj.DotSettings index ced14c154..c7410bba2 100644 --- a/API/API.csproj.DotSettings +++ b/API/API.csproj.DotSettings @@ -1,4 +1,3 @@  True - True True \ No newline at end of file diff --git a/API/Assets/anilist-no-image-placeholder.jpg b/API/Assets/anilist-no-image-placeholder.jpg deleted file mode 100644 index 54c1066b6..000000000 Binary files a/API/Assets/anilist-no-image-placeholder.jpg and /dev/null differ diff --git a/API/Comparators/ChapterSortComparer.cs b/API/Comparators/ChapterSortComparer.cs index f5d566cb1..55572aa7e 100644 --- a/API/Comparators/ChapterSortComparer.cs +++ b/API/Comparators/ChapterSortComparer.cs @@ -1,34 +1,30 @@ using System.Collections.Generic; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; namespace API.Comparators; -#nullable enable - /// -/// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST. +/// Sorts chapters based on their Number. Uses natural ordering of doubles. /// -public class ChapterSortComparerDefaultLast : IComparer +public class ChapterSortComparer : IComparer { /// - /// Normal sort for 2 doubles. DefaultChapterNumber always comes last + /// Normal sort for 2 doubles. 0 always comes last /// /// /// /// - public int Compare(float x, float y) + public int Compare(double x, double y) { - if (x.Is(Parser.DefaultChapterNumber) && y.Is(Parser.DefaultChapterNumber)) return 0; + if (x == 0.0 && y == 0.0) return 0; // if x is 0, it comes second - if (x.Is(Parser.DefaultChapterNumber)) return 1; + if (x == 0.0) return 1; // if y is 0, it comes second - if (y.Is(Parser.DefaultChapterNumber)) return -1; + if (y == 0.0) return -1; return x.CompareTo(y); } - public static readonly ChapterSortComparerDefaultLast Default = new ChapterSortComparerDefaultLast(); + public static readonly ChapterSortComparer Default = new ChapterSortComparer(); } /// @@ -38,43 +34,33 @@ public class ChapterSortComparerDefaultLast : IComparer /// This is represented by Chapter 0, Chapter 81. /// /// -public class ChapterSortComparerDefaultFirst : IComparer +public class ChapterSortComparerZeroFirst : IComparer { - public int Compare(float x, float y) + public int Compare(double x, double y) { - if (x.Is(Parser.DefaultChapterNumber) && y.Is(Parser.DefaultChapterNumber)) return 0; + if (x == 0.0 && y == 0.0) return 0; // if x is 0, it comes first - if (x.Is(Parser.DefaultChapterNumber)) return -1; + if (x == 0.0) return -1; // if y is 0, it comes first - if (y.Is(Parser.DefaultChapterNumber)) return 1; + if (y == 0.0) return 1; return x.CompareTo(y); } - public static readonly ChapterSortComparerDefaultFirst Default = new ChapterSortComparerDefaultFirst(); + public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst(); } -/// -/// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST. -/// -public class ChapterSortComparerSpecialsLast : IComparer +public class SortComparerZeroLast : IComparer { - /// - /// Normal sort for 2 doubles. DefaultSpecialNumber always comes last - /// - /// - /// - /// - public int Compare(float x, float y) + public int Compare(double x, double y) { - if (x.Is(Parser.SpecialVolumeNumber) && y.Is(Parser.SpecialVolumeNumber)) return 0; - // if x is 0, it comes second - if (x.Is(Parser.SpecialVolumeNumber)) return 1; - // if y is 0, it comes second - if (y.Is(Parser.SpecialVolumeNumber)) return -1; + if (x == 0.0 && y == 0.0) return 0; + // if x is 0, it comes last + if (x == 0.0) return 1; + // if y is 0, it comes last + if (y == 0.0) return -1; return x.CompareTo(y); } - - public static readonly ChapterSortComparerSpecialsLast Default = new ChapterSortComparerSpecialsLast(); + public static readonly SortComparerZeroLast Default = new SortComparerZeroLast(); } diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs index 17eeee059..194d013ea 100644 --- a/API/Comparators/NumericComparer.cs +++ b/API/Comparators/NumericComparer.cs @@ -2,8 +2,6 @@ namespace API.Comparators; -#nullable enable - public class NumericComparer : IComparer { diff --git a/API/Comparators/StringLogicalComparer.cs b/API/Comparators/StringLogicalComparer.cs index 6759454fb..805f85623 100644 --- a/API/Comparators/StringLogicalComparer.cs +++ b/API/Comparators/StringLogicalComparer.cs @@ -6,7 +6,6 @@ using static System.Char; namespace API.Comparators; - public static class StringLogicalComparer { public static int Compare(string s1, string s2) diff --git a/API/Constants/CacheProfiles.cs b/API/Constants/CacheProfiles.cs index afc82f19c..9c702a0d8 100644 --- a/API/Constants/CacheProfiles.cs +++ b/API/Constants/CacheProfiles.cs @@ -8,31 +8,19 @@ public static class EasyCacheProfiles public const string RevokedJwt = "revokedJWT"; public const string Favicon = "favicon"; /// - /// Images for Publishers - /// - public const string Publisher = "publisherImages"; - /// /// If a user's license is valid /// public const string License = "license"; /// - /// License Information - /// - public const string LicenseInfo = "licenseInfo"; - /// /// Cache the libraries on the server /// public const string Library = "library"; /// - /// External Series metadata for Kavita+ recommendation + /// Metadata filter /// + public const string Filter = "filter"; + public const string KavitaPlusReviews = "kavita+reviews"; + public const string KavitaPlusRecommendations = "kavita+recommendations"; + public const string KavitaPlusRatings = "kavita+ratings"; public const string KavitaPlusExternalSeries = "kavita+externalSeries"; - /// - /// Match Series metadata for Kavita+ metadata download - /// - public const string KavitaPlusMatchSeries = "kavita+matchSeries"; - /// - /// All Locales on the Server - /// - public const string LocaleOptions = "locales"; } diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs index 1be979a56..69de1821b 100644 --- a/API/Constants/PolicyConstants.cs +++ b/API/Constants/PolicyConstants.cs @@ -35,19 +35,7 @@ public static class PolicyConstants /// Used to give a user ability to Login to their account /// public const string LoginRole = "Login"; - /// - /// Restricts the ability to manage their account without an admin - /// - /// This is used explicitly for Demo Server. Not sure why it would be used in another fashion - public const string ReadOnlyRole = "Read Only"; - /// - /// Ability to promote entities (Collections, Reading Lists, etc). - /// - public const string PromoteRole = "Promote"; - - - public static readonly ImmutableArray ValidRoles = - ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole, ReadOnlyRole, PromoteRole); + ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole); } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index d8b9164af..a48532c32 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -16,7 +15,6 @@ using API.Errors; using API.Extensions; using API.Helpers.Builders; using API.Services; -using API.Services.Plus; using API.SignalR; using AutoMapper; using Hangfire; @@ -28,20 +26,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SharpCompress; namespace API.Controllers; -#nullable enable - /// /// All Account matters /// public class AccountController : BaseApiController { - // Hardcoded to avoid localization multiple enumeration: https://github.com/Kareadita/Kavita/issues/2829 - private const string BadCredentialsMessage = "Your credentials are not correct"; - private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly ITokenService _tokenService; @@ -82,12 +74,10 @@ public class AccountController : BaseApiController [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { + _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); + var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName); if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system - - _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin)) @@ -138,24 +128,13 @@ public class AccountController : BaseApiController return BadRequest(usernameValidation); } - // If Email is empty, default to the username - if (string.IsNullOrEmpty(registerDto.Email)) - { - registerDto.Email = registerDto.Username; - } - var user = new AppUserBuilder(registerDto.Username, registerDto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + var result = await _userManager.CreateAsync(user, registerDto.Password); if (!result.Succeeded) return BadRequest(result.Errors); - // Assign default streams - AddDefaultStreamsToUser(user); - - // Assign default reading profile - await AddDefaultReadingProfileToUser(user); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token)); @@ -203,15 +182,13 @@ public class AccountController : BaseApiController { user = await _userManager.Users .Include(u => u.UserPreferences) - .AsSplitQuery() .SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey); } else { user = await _userManager.Users .Include(u => u.UserPreferences) - .AsSplitQuery() - .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant()); + .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper()); } _logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username, HttpContext.Connection.RemoteIpAddress?.ToString()); @@ -219,7 +196,7 @@ public class AccountController : BaseApiController if (user == null) { _logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username); - return Unauthorized(BadCredentialsMessage); + return Unauthorized(await _localizationService.Get("en", "bad-credentials")); } var roles = await _userManager.GetRolesAsync(user); if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); @@ -240,10 +217,10 @@ public class AccountController : BaseApiController if (!result.Succeeded) { - string errorStr = result.IsNotAllowed - ? await _localizationService.Translate(user.Id, "confirm-email") - : BadCredentialsMessage; - _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); + var errorStr = await _localizationService.Translate(user.Id, + result.IsNotAllowed ? "confirm-email" : "bad-credentials"); + _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, + errorStr); return Unauthorized(errorStr); } } @@ -336,36 +313,33 @@ public class AccountController : BaseApiController [HttpPost("reset-api-key")] public async Task> ResetApiKey() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException(); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) throw new KavitaUnauthenticatedUserException(); + user.ApiKey = HashUtil.ApiKey(); if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) { - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, - MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); return Ok(user.ApiKey); } await _unitOfWork.RollbackAsync(); return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key")); + } /// - /// Initiates the flow to update a user's email address. - /// - /// If email is not setup, then the email address is not changed in this API. A confirmation link is sent/dumped which will + /// Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will /// validate the email. It must be confirmed for the email to update. /// /// /// Returns just if the email was sent or server isn't reachable [HttpPost("update/email")] - public async Task> UpdateEmail(UpdateEmailDto? dto) + public async Task UpdateEmail(UpdateEmailDto? dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole)) - return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload")); @@ -374,13 +348,12 @@ public class AccountController : BaseApiController // Validate this user's password if (! await _userManager.CheckPasswordAsync(user, dto.Password)) { - _logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); + _logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); } // Validate no other users exist with this email - if (user.Email!.Equals(dto.Email)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); + if (user.Email!.Equals(dto.Email)) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); // Check if email is used by another user var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); @@ -397,66 +370,51 @@ public class AccountController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token")); } - var isValidEmailAddress = _emailService.IsValidEmail(user.Email); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var shouldEmailUser = serverSettings.IsEmailSetup() || !isValidEmailAddress; - - user.EmailConfirmed = !shouldEmailUser; + user.EmailConfirmed = false; user.ConfirmationToken = token; await _userManager.UpdateAsync(user); - var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); - _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - - if (!shouldEmailUser) - { - _logger.LogInformation("Cannot email admin, email not setup or admin email invalid"); - return Ok(new InviteUserResponse - { - EmailLink = string.Empty, - EmailSent = false, - InvalidEmail = !isValidEmailAddress - }); - } - - // Send a confirmation email try { - if (!isValidEmailAddress) + var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); + _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + + if (!_emailService.IsValidEmail(user.Email)) { _logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); return Ok(new InviteUserResponse { EmailLink = string.Empty, - EmailSent = false, - InvalidEmail = true, + EmailSent = false }); } - try + var accessible = await _accountService.CheckIfAccessible(Request); + if (accessible) { - var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; - // Email the old address of the update change - BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() + try { - EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, - InstallId = BuildInfo.Version.ToString(), - InvitingUser = invitingUser, - ServerConfirmationLink = emailLink - })); - } - catch (Exception) - { - /* Swallow exception */ + // Email the old address of the update change + await _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() + { + EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, + InstallId = BuildInfo.Version.ToString(), + InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!, + ServerConfirmationLink = emailLink + }); + } + catch (Exception) + { + /* Swallow exception */ + } } return Ok(new InviteUserResponse { EmailLink = string.Empty, - EmailSent = true, - InvalidEmail = !isValidEmailAddress + EmailSent = accessible }); } catch (Exception ex) @@ -474,10 +432,9 @@ public class AccountController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - if (!await _accountService.CanChangeAgeRestriction(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating; user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns; @@ -512,7 +469,6 @@ public class AccountController : BaseApiController var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (adminUser == null) return Unauthorized(); if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user")); @@ -524,22 +480,6 @@ public class AccountController : BaseApiController var errors = await _accountService.ValidateUsername(dto.Username); if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "username-taken")); user.UserName = dto.Username; - await _userManager.UpdateNormalizedUserNameAsync(user); - _unitOfWork.UserRepository.Update(user); - } - - // Check if email is changing for a non-admin user - var isUpdatingAnotherAccount = user.Id != adminUser.Id; - if (isUpdatingAnotherAccount && !string.IsNullOrEmpty(dto.Email) && user.Email != dto.Email) - { - // Validate username change - var errors = await _accountService.ValidateEmail(dto.Email); - if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "email-taken")); - - user.Email = dto.Email; - user.EmailConfirmed = true; // When an admin performs the flow, we assume the email address is able to receive data - - await _userManager.UpdateNormalizedEmailAsync(user); _unitOfWork.UserRepository.Update(user); } @@ -612,7 +552,7 @@ public class AccountController : BaseApiController } /// - /// Requests the Invite Url for the AppUserId. Will return error if user is already validated. + /// Requests the Invite Url for the UserId. Will return error if user is already validated. /// /// /// Include the "https://ip:port/" in the generated link @@ -628,12 +568,13 @@ public class AccountController : BaseApiController if (string.IsNullOrEmpty(user.ConfirmationToken)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail")); - return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); + return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } /// - /// Invites a user to the server. Will generate a setup link for continuing setup. If email is not setup, a link will be presented to user to continue setup. + /// Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no + /// email will be sent. /// /// /// @@ -646,7 +587,8 @@ public class AccountController : BaseApiController if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied")); dto.Email = dto.Email.Trim(); - if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); + if (string.IsNullOrEmpty(dto.Email)) + return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); _logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); @@ -661,20 +603,13 @@ public class AccountController : BaseApiController } // Create a new user - var user = new AppUserBuilder(dto.Email, dto.Email, - await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); _unitOfWork.UserRepository.Add(user); try { var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword); if (!result.Succeeded) return BadRequest(result.Errors); - // Assign default streams - AddDefaultStreamsToUser(user); - - // Assign default reading profile - await AddDefaultReadingProfileToUser(user); - // Assign Roles var roles = dto.Roles; var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole); @@ -712,6 +647,7 @@ public class AccountController : BaseApiController user.CreateSideNavFromLibrary(lib); } + _unitOfWork.UserRepository.Update(user); user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; @@ -723,7 +659,6 @@ public class AccountController : BaseApiController } user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); } catch (Exception ex) @@ -731,37 +666,42 @@ public class AccountController : BaseApiController _logger.LogError(ex, "There was an error during invite user flow, unable to create user. Deleting user for retry"); _unitOfWork.UserRepository.Delete(user); await _unitOfWork.CommitAsync(); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user")); } + try { - var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); + var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + _logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup()) + if (!_emailService.IsValidEmail(dto.Email)) { - _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty)); + _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email); return Ok(new InviteUserResponse { EmailLink = emailLink, - EmailSent = false, - InvalidEmail = true + EmailSent = false }); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + var accessible = await _accountService.CheckIfAccessible(Request); + if (accessible) { - EmailAddress = dto.Email, - InvitingUser = adminUser.UserName, - ServerConfirmationLink = emailLink - })); + // Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails + BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto() + { + EmailAddress = dto.Email, + InvitingUser = adminUser.UserName!, + ServerConfirmationLink = emailLink + })); + + } return Ok(new InviteUserResponse { EmailLink = emailLink, - EmailSent = true + EmailSent = accessible }); } catch (Exception ex) @@ -772,29 +712,6 @@ public class AccountController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user")); } - private void AddDefaultStreamsToUser(AppUser user) - { - foreach (var newStream in Seed.DefaultStreams.Select(stream => _mapper.Map(stream))) - { - user.DashboardStreams.Add(newStream); - } - - foreach (var stream in Seed.DefaultSideNavStreams.Select(stream => _mapper.Map(stream))) - { - user.SideNavStreams.Add(stream); - } - } - - private async Task AddDefaultReadingProfileToUser(AppUser user) - { - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Default Profile") - .WithKind(ReadingProfileKind.Default) - .Build(); - _unitOfWork.AppUserReadingProfileRepository.Add(profile); - await _unitOfWork.CommitAsync(); - } - /// /// Last step in authentication flow, confirms the email token for email /// @@ -819,7 +736,6 @@ public class AccountController : BaseApiController { validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username)); } - validationErrors.AddRange(await _accountService.ValidatePassword(user, dto.Password)); if (validationErrors.Any()) @@ -891,7 +807,6 @@ public class AccountController : BaseApiController return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update")); } user.ConfirmationToken = null; - user.EmailConfirmed = true; await _unitOfWork.CommitAsync(); @@ -899,6 +814,7 @@ public class AccountController : BaseApiController await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + // Perform Login code return Ok(); } @@ -907,19 +823,19 @@ public class AccountController : BaseApiController public async Task> ConfirmForgotPassword(ConfirmPasswordResetDto dto) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (user == null) - { - return BadRequest(BadCredentialsMessage); - } - try { + if (user == null) + { + return BadRequest(await _localizationService.Get("en", "bad-credentials")); + } + var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", dto.Token); if (!result) { _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto); - return BadRequest(BadCredentialsMessage); + return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials")); } var errors = await _accountService.ChangeUserPassword(user, dto.Password); @@ -943,7 +859,6 @@ public class AccountController : BaseApiController [EnableRateLimiting("Authentication")] public async Task> ForgotPassword([FromQuery] string email) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); if (user == null) { @@ -952,37 +867,32 @@ public class AccountController : BaseApiController } var roles = await _userManager.GetRolesAsync(user); - if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole)) + if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole)) return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied")); if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "confirm-email")); - - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); - user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - - if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled")); if (!_emailService.IsValidEmail(user.Email)) { - _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI or from url above", user.Email); + _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send", user.Email); return Ok(await _localizationService.Translate(user.Id, "invalid-email")); } - - var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value; - BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto() + if (await _accountService.CheckIfAccessible(Request)) { - EmailAddress = user.Email, - ServerConfirmationLink = emailLink, - InstallId = installId - })); + await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto() + { + EmailAddress = user.Email, + ServerConfirmationLink = emailLink, + InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value + }); + return Ok(await _localizationService.Translate(user.Id, "email-sent")); + } - return Ok(await _localizationService.Translate(user.Id, "email-sent")); + return Ok(await _localizationService.Translate(user.Id, "not-accessible-password")); } [HttpGet("email-confirmed")] @@ -999,12 +909,12 @@ public class AccountController : BaseApiController public async Task> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (user == null) return BadRequest(BadCredentialsMessage); + if (user == null) return BadRequest(await _localizationService.Get("en", "bad-credentials")); if (!await ConfirmEmailToken(dto.Token, user)) { _logger.LogInformation("confirm-migration-email email token is invalid"); - return BadRequest(BadCredentialsMessage); + return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials")); } await _unitOfWork.CommitAsync(); @@ -1030,10 +940,9 @@ public class AccountController : BaseApiController /// /// /// - [Authorize("RequireAdminRole")] [HttpPost("resend-confirmation-email")] [EnableRateLimiting("Authentication")] - public async Task> ResendConfirmationSendEmail([FromQuery] int userId) + public async Task> ResendConfirmationSendEmail([FromQuery] int userId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return BadRequest(await _localizationService.Get("en", "no-user")); @@ -1043,50 +952,98 @@ public class AccountController : BaseApiController await _localizationService.Translate(user.Id, "user-migration-needed")); if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed")); - // TODO: If the target user is read only, we might want to just forgo this - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email); - _logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email); + _logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink); + _logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token); if (!_emailService.IsValidEmail(user.Email)) { - _logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email); + _logger.LogCritical("[Email Migration]: User is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.Email); + return Ok(await _localizationService.Translate(user.Id, "invalid-email")); } - - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email); - - if (!shouldEmailUser) + if (await _accountService.CheckIfAccessible(Request)) { - return Ok(new InviteUserResponse() + try { - EmailLink = emailLink, - EmailSent = false, - InvalidEmail = !_emailService.IsValidEmail(user.Email) - }); + await _emailService.SendMigrationEmail(new EmailMigrationDto() + { + EmailAddress = user.Email!, + Username = user.UserName!, + ServerConfirmationLink = emailLink, + InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue resending invite email"); + return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email")); + } + return Ok(emailLink); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() - { - EmailAddress = user.Email!, - InvitingUser = User.GetUsername(), - ServerConfirmationLink = emailLink, - InstallId = serverSettings.InstallId - })); - - return Ok(new InviteUserResponse() - { - EmailLink = emailLink, - EmailSent = true, - InvalidEmail = !_emailService.IsValidEmail(user.Email) - }); + return Ok(await _localizationService.Translate(user.Id, "not-accessible")); } + /// + /// This is similar to invite. Essentially we authenticate the user's password then go through invite email flow + /// + /// + /// + [AllowAnonymous] + [HttpPost("migrate-email")] + public async Task> MigrateEmail(MigrateUserEmailDto dto) + { + // If there is an admin account already, return + var users = await _unitOfWork.UserRepository.GetAdminUsersAsync(); + if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists")); + + // Check if there is an existing invite + var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); + if (emailValidationErrors.Any()) + { + var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName)); + + _logger.LogInformation("A user is attempting to login, but hasn't accepted email invite"); + return BadRequest(await _localizationService.Get("en", "user-already-invited")); + } + + + var user = await _userManager.Users + .Include(u => u.UserPreferences) + .SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper()); + if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username")); + + var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password); + if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials")); + + try + { + var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + + user.Email = dto.Email; + if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "critical-email-migration")); + _unitOfWork.UserRepository.Update(user); + + await _unitOfWork.CommitAsync(); + + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue during email migration. Contact support"); + _unitOfWork.UserRepository.Delete(user); + await _unitOfWork.CommitAsync(); + } + + return BadRequest(await _localizationService.Get("en", "critical-email-migration")); + } + + + private async Task ConfirmEmailToken(string token, AppUser user) { var result = await _userManager.ConfirmEmailAsync(user, token); @@ -1125,26 +1082,12 @@ public class AccountController : BaseApiController baseUrl = baseUrl.Replace("//", "/"); } - if (baseUrl.StartsWith('/')) + if (baseUrl.StartsWith("/")) { baseUrl = baseUrl.Substring(1, baseUrl.Length - 1); } } return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey); - } - - /// - /// Is the user's current email valid or not - /// - /// - [HttpGet("is-email-valid")] - public async Task> IsEmailValid() - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); - if (user == null) return Unauthorized(); - if (string.IsNullOrEmpty(user.Email)) return Ok(false); - - return Ok(_emailService.IsValidEmail(user.Email)); } } diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 4f8edd511..25bde9ddb 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -1,10 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.ManualMigrations; -using API.DTOs; -using API.DTOs.Progress; +using System.Threading.Tasks; using API.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -12,8 +6,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - public class AdminController : BaseApiController { private readonly UserManager _userManager; @@ -31,7 +23,7 @@ public class AdminController : BaseApiController [HttpGet("exists")] public async Task> AdminExists() { - var users = await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); + var users = await _userManager.GetUsersInRoleAsync("Admin"); return users.Count > 0; } } diff --git a/API/Controllers/BaseApiController.cs b/API/Controllers/BaseApiController.cs index 7806ef660..2ac2b5cce 100644 --- a/API/Controllers/BaseApiController.cs +++ b/API/Controllers/BaseApiController.cs @@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - [ApiController] [Route("api/[controller]")] [Authorize] diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index e1d7da9e8..f2b351a65 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -14,8 +14,6 @@ using VersOne.Epub; namespace API.Controllers; -#nullable enable - public class BookController : BaseApiController { private readonly IBookService _bookService; @@ -50,7 +48,7 @@ public class BookController : BaseApiController case MangaFormat.Epub: { var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; - using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions); + using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions); bookTitle = book.Title; break; } @@ -101,8 +99,8 @@ public class BookController : BaseApiController if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); + using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); - using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.LenientBookReaderOptions); var key = BookService.CoalesceKeyForAnyFile(book, file); if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing")); diff --git a/API/Controllers/CBLController.cs b/API/Controllers/CBLController.cs index 150628ced..5ff82edb7 100644 --- a/API/Controllers/CBLController.cs +++ b/API/Controllers/CBLController.cs @@ -2,18 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using API.Constants; using API.DTOs.ReadingLists.CBL; using API.Extensions; using API.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; namespace API.Controllers; -#nullable enable - /// /// Responsible for the CBL import flow /// @@ -21,40 +17,35 @@ public class CblController : BaseApiController { private readonly IReadingListService _readingListService; private readonly IDirectoryService _directoryService; - private readonly ILocalizationService _localizationService; - public CblController(IReadingListService readingListService, IDirectoryService directoryService, ILocalizationService localizationService) + public CblController(IReadingListService readingListService, IDirectoryService directoryService) { _readingListService = readingListService; _directoryService = directoryService; - _localizationService = localizationService; } /// /// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful. /// If this returns errors, the cbl will always be rejected by Kavita. /// - /// FormBody with parameter name of cbl - /// Use comic vine matching or not. Defaults to false + /// FormBody with parameter name of cbl /// [HttpPost("validate")] - [SwaggerIgnore] - public async Task> ValidateCbl(IFormFile cbl, [FromQuery] bool useComicVineMatching = false) + public async Task> ValidateCbl([FromForm(Name = "cbl")] IFormFile file) { var userId = User.GetUserId(); try { - var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); - importSummary.FileName = cbl.FileName; - + var cbl = await SaveAndLoadCblFile(file); + var importSummary = await _readingListService.ValidateCblFile(userId, cbl); + importSummary.FileName = file.FileName; return Ok(importSummary); } catch (ArgumentNullException) { return Ok(new CblImportSummaryDto() { - FileName = cbl.FileName, + FileName = file.FileName, Success = CblImportResult.Fail, Results = new List() { @@ -69,7 +60,7 @@ public class CblController : BaseApiController { return Ok(new CblImportSummaryDto() { - FileName = cbl.FileName, + FileName = file.FileName, Success = CblImportResult.Fail, Results = new List() { @@ -86,29 +77,24 @@ public class CblController : BaseApiController /// /// Performs the actual import (assuming dryRun = false) /// - /// FormBody with parameter name of cbl + /// FormBody with parameter name of cbl /// If true, will only emulate the import but not perform. This should be done to preview what will happen - /// Use comic vine matching or not. Defaults to false /// [HttpPost("import")] - [SwaggerIgnore] - public async Task> ImportCbl(IFormFile cbl, [FromQuery] bool dryRun = false, [FromQuery] bool useComicVineMatching = false) + public async Task> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - try { var userId = User.GetUserId(); - var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching); - importSummary.FileName = cbl.FileName; - + var cbl = await SaveAndLoadCblFile(file); + var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun); + importSummary.FileName = file.FileName; return Ok(importSummary); } catch (ArgumentNullException) { return Ok(new CblImportSummaryDto() { - FileName = cbl.FileName, + FileName = file.FileName, Success = CblImportResult.Fail, Results = new List() { @@ -123,7 +109,7 @@ public class CblController : BaseApiController { return Ok(new CblImportSummaryDto() { - FileName = cbl.FileName, + FileName = file.FileName, Success = CblImportResult.Fail, Results = new List() { diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs deleted file mode 100644 index 94535d499..000000000 --- a/API/Controllers/ChapterController.cs +++ /dev/null @@ -1,458 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using AutoMapper; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Nager.ArticleNumber; - -namespace API.Controllers; - -public class ChapterController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger, - IMapper mapper) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - _logger = logger; - _mapper = mapper; - } - - /// - /// Gets a single chapter - /// - /// - /// - [HttpGet] - public async Task> GetChapter(int chapterId) - { - var chapter = - await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, - ChapterIncludes.People | ChapterIncludes.Files); - - return Ok(chapter); - } - - /// - /// Removes a Chapter - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpDelete] - public async Task> DeleteChapter(int chapterId) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, - ChapterIncludes.Files | ChapterIncludes.ExternalReviews | ChapterIncludes.ExternalRatings); - if (chapter == null) - return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); - - var vol = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId, VolumeIncludes.Chapters); - if (vol == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); - - // If there is only 1 chapter within the volume, then we need to remove the volume - var needToRemoveVolume = vol.Chapters.Count == 1; - if (needToRemoveVolume) - { - _unitOfWork.VolumeRepository.Remove(vol); - } - else - { - _unitOfWork.ChapterRepository.Remove(chapter); - } - - // If we removed the volume, do an additional check if we need to delete the actual series as well or not - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); - var needToRemoveSeries = needToRemoveVolume && series != null && series.Volumes.Count <= 1; - if (needToRemoveSeries) - { - _unitOfWork.SeriesRepository.Remove(series!); - } - - - - if (!await _unitOfWork.CommitAsync()) return Ok(false); - - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false); - if (needToRemoveVolume) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); - } - - if (needToRemoveSeries) - { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, - MessageFactory.SeriesRemovedEvent(series!.Id, series.Name, series.LibraryId), false); - } - - return Ok(true); - } - - /// - /// Deletes multiple chapters and any volumes with no leftover chapters - /// - /// The ID of the series - /// The IDs of the chapters to be deleted - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("delete-multiple")] - public async Task> DeleteMultipleChapters([FromQuery] int seriesId, DeleteChaptersDto dto) - { - try - { - var chapterIds = dto.ChapterIds; - if (chapterIds == null || chapterIds.Count == 0) - { - return BadRequest("ChapterIds required"); - } - - // Fetch all chapters to be deleted - var chapters = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds)).ToList(); - - // Group chapters by their volume - var volumesToUpdate = chapters.GroupBy(c => c.VolumeId).ToList(); - var removedVolumes = new List(); - - foreach (var volumeGroup in volumesToUpdate) - { - var volumeId = volumeGroup.Key; - var chaptersToDelete = volumeGroup.ToList(); - - // Fetch the volume - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, VolumeIncludes.Chapters); - if (volume == null) - return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); - - // Check if all chapters in the volume are being deleted - var isVolumeToBeRemoved = volume.Chapters.Count == chaptersToDelete.Count; - - if (isVolumeToBeRemoved) - { - _unitOfWork.VolumeRepository.Remove(volume); - removedVolumes.Add(volume.Id); - } - else - { - // Remove only the specified chapters - _unitOfWork.ChapterRepository.Remove(chaptersToDelete); - } - } - - if (!await _unitOfWork.CommitAsync()) return Ok(false); - - // Send events for removed chapters - foreach (var chapter in chapters) - { - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, - MessageFactory.ChapterRemovedEvent(chapter.Id, seriesId), false); - } - - // Send events for removed volumes - foreach (var volumeId in removedVolumes) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, - MessageFactory.VolumeRemovedEvent(volumeId, seriesId), false); - } - - return Ok(true); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occured while deleting chapters"); - return BadRequest(_localizationService.Translate(User.GetUserId(), "generic-error")); - } - - } - - - /// - /// Update chapter metadata - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("update")] - public async Task UpdateChapterMetadata(UpdateChapterDto dto) - { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, - ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags); - if (chapter == null) - return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); - - if (chapter.AgeRating != dto.AgeRating) - { - chapter.AgeRating = dto.AgeRating; - chapter.KPlusOverrides.Remove(MetadataSettingField.AgeRating); - } - - dto.Summary ??= string.Empty; - - if (chapter.Summary != dto.Summary.Trim()) - { - chapter.Summary = dto.Summary.Trim(); - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterSummary); - } - - if (chapter.Language != dto.Language) - { - chapter.Language = dto.Language ?? string.Empty; - } - - if (chapter.SortOrder.IsNot(dto.SortOrder)) - { - chapter.SortOrder = dto.SortOrder; // TODO: Figure out validation - } - - if (chapter.TitleName != dto.TitleName) - { - chapter.TitleName = dto.TitleName; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterTitle); - } - - if (chapter.ReleaseDate != dto.ReleaseDate) - { - chapter.ReleaseDate = dto.ReleaseDate; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterReleaseDate); - } - - if (!string.IsNullOrEmpty(dto.ISBN) && ArticleNumberHelper.IsValidIsbn10(dto.ISBN) || - ArticleNumberHelper.IsValidIsbn13(dto.ISBN)) - { - chapter.ISBN = dto.ISBN; - } - - if (string.IsNullOrEmpty(dto.WebLinks)) - { - chapter.WebLinks = string.Empty; - } else - { - chapter.WebLinks = string.Join(',', dto.WebLinks - .Split(',') - .Where(s => !string.IsNullOrEmpty(s)) - .Select(s => s.Trim())! - ); - } - - - #region Genres - chapter.Genres ??= []; - await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork); - #endregion - - #region Tags - chapter.Tags ??= []; - await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork); - #endregion - - #region People - chapter.People ??= []; - - // Update writers - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Writers.Select(p => p.Name).ToList(), - PersonRole.Writer, - _unitOfWork - ); - - // Update characters - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Characters.Select(p => p.Name).ToList(), - PersonRole.Character, - _unitOfWork - ); - - // Update pencillers - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Pencillers.Select(p => p.Name).ToList(), - PersonRole.Penciller, - _unitOfWork - ); - - // Update inkers - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Inkers.Select(p => p.Name).ToList(), - PersonRole.Inker, - _unitOfWork - ); - - // Update colorists - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Colorists.Select(p => p.Name).ToList(), - PersonRole.Colorist, - _unitOfWork - ); - - // Update letterers - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Letterers.Select(p => p.Name).ToList(), - PersonRole.Letterer, - _unitOfWork - ); - - // Update cover artists - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.CoverArtists.Select(p => p.Name).ToList(), - PersonRole.CoverArtist, - _unitOfWork - ); - - // Update editors - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Editors.Select(p => p.Name).ToList(), - PersonRole.Editor, - _unitOfWork - ); - - // TODO: Only remove field if changes were made - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); - // Update publishers - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Publishers.Select(p => p.Name).ToList(), - PersonRole.Publisher, - _unitOfWork - ); - - // Update translators - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Translators.Select(p => p.Name).ToList(), - PersonRole.Translator, - _unitOfWork - ); - - // Update imprints - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Imprints.Select(p => p.Name).ToList(), - PersonRole.Imprint, - _unitOfWork - ); - - // Update teams - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Teams.Select(p => p.Name).ToList(), - PersonRole.Team, - _unitOfWork - ); - - // Update locations - await PersonHelper.UpdateChapterPeopleAsync( - chapter, - dto.Locations.Select(p => p.Name).ToList(), - PersonRole.Location, - _unitOfWork - ); - #endregion - - #region Locks - chapter.AgeRatingLocked = dto.AgeRatingLocked; - chapter.LanguageLocked = dto.LanguageLocked; - chapter.TitleNameLocked = dto.TitleNameLocked; - chapter.SortOrderLocked = dto.SortOrderLocked; - chapter.GenresLocked = dto.GenresLocked; - chapter.TagsLocked = dto.TagsLocked; - chapter.CharacterLocked = dto.CharacterLocked; - chapter.ColoristLocked = dto.ColoristLocked; - chapter.EditorLocked = dto.EditorLocked; - chapter.InkerLocked = dto.InkerLocked; - chapter.ImprintLocked = dto.ImprintLocked; - chapter.LettererLocked = dto.LettererLocked; - chapter.PencillerLocked = dto.PencillerLocked; - chapter.PublisherLocked = dto.PublisherLocked; - chapter.TranslatorLocked = dto.TranslatorLocked; - chapter.CoverArtistLocked = dto.CoverArtistLocked; - chapter.WriterLocked = dto.WriterLocked; - chapter.SummaryLocked = dto.SummaryLocked; - chapter.ISBNLocked = dto.ISBNLocked; - chapter.ReleaseDateLocked = dto.ReleaseDateLocked; - #endregion - - - _unitOfWork.ChapterRepository.Update(chapter); - - if (!_unitOfWork.HasChanges()) - { - return Ok(); - } - - // TODO: Emit a ChapterMetadataUpdate out - - await _unitOfWork.CommitAsync(); - - - return Ok(); - } - - /// - /// Returns Ratings and Reviews for an individual Chapter - /// - /// - /// - [HttpGet("chapter-detail-plus")] - public async Task> ChapterDetailPlus([FromQuery] int chapterId) - { - var ret = new ChapterDetailPlusDto(); - - var userReviews = (await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId())) - .Where(r => !string.IsNullOrEmpty(r.Body)) - .OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0) - .ToList(); - - var ownRating = await _unitOfWork.UserRepository.GetUserChapterRatingAsync(User.GetUserId(), chapterId); - if (ownRating != null) - { - ret.Rating = ownRating.Rating; - ret.HasBeenRated = ownRating.HasBeenRated; - } - - var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); - if (externalReviews.Count > 0) - { - userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(externalReviews)); - } - - ret.Reviews = userReviews; - - ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); - - return Ok(ret); - } - -} diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 2c0abc609..4f5f955be 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -1,27 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs.Collection; using API.DTOs.CollectionTags; -using API.Entities; +using API.Entities.Metadata; using API.Extensions; -using API.Helpers.Builders; using API.Services; -using API.Services.Plus; -using API.SignalR; -using Hangfire; using Kavita.Common; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// APIs for Collections /// @@ -30,70 +21,61 @@ public class CollectionController : BaseApiController private readonly IUnitOfWork _unitOfWork; private readonly ICollectionTagService _collectionService; private readonly ILocalizationService _localizationService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly ISmartCollectionSyncService _collectionSyncService; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; /// public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService, - ILocalizationService localizationService, IExternalMetadataService externalMetadataService, - ISmartCollectionSyncService collectionSyncService, ILogger logger, - IEventHub eventHub) + ILocalizationService localizationService) { _unitOfWork = unitOfWork; _collectionService = collectionService; _localizationService = localizationService; - _externalMetadataService = externalMetadataService; - _collectionSyncService = collectionSyncService; - _logger = logger; - _eventHub = eventHub; } /// - /// Returns all Collection tags for a given User + /// Return a list of all collection tags on the server for the logged in user. /// /// [HttpGet] - public async Task>> GetAllTags(bool ownedOnly = false) + public async Task>> GetAllTags() { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(User.GetUserId(), !ownedOnly)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) return Unauthorized(); + var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + if (isAdmin) + { + return Ok(await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()); + } + + return Ok(await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id)); } /// - /// Returns a single Collection tag by Id for a given user + /// Searches against the collection tags on the DB and returns matches that meet the search criteria. + /// Search strings will be cleaned of certain fields, like % /// - /// + /// Search term /// - [HttpGet("single")] - public async Task>> GetTag(int collectionId) + [Authorize(Policy = "RequireAdminRole")] + [HttpGet("search")] + public async Task>> SearchTags(string queryString) { - var collections = await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(User.GetUserId(), false); - return Ok(collections.FirstOrDefault(c => c.Id == collectionId)); - } + queryString ??= string.Empty; + queryString = queryString.Replace(@"%", string.Empty); + if (queryString.Length == 0) return await GetAllTags(); - /// - /// Returns all collections that contain the Series for the user with the option to allow for promoted collections (non-user owned) - /// - /// - /// - /// - [HttpGet("all-series")] - public async Task>> GetCollectionsBySeries(int seriesId, bool ownedOnly = false) - { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosBySeriesAsync(User.GetUserId(), seriesId, !ownedOnly)); + return Ok(await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId())); } - /// /// Checks if a collection exists with the name /// /// If empty or null, will return true as that is invalid /// + [Authorize(Policy = "RequireAdminRole")] [HttpGet("name-exists")] public async Task> DoesNameExists(string name) { - return Ok(await _unitOfWork.CollectionTagRepository.CollectionExists(name, User.GetUserId())); + return Ok(await _collectionService.TagExistsByName(name)); } /// @@ -102,19 +84,13 @@ public class CollectionController : BaseApiController /// /// /// + [Authorize(Policy = "RequireAdminRole")] [HttpPost("update")] - public async Task UpdateTag(AppUserCollectionDto updatedTag) + public async Task UpdateTag(CollectionTagDto updatedTag) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - try { - if (await _collectionService.UpdateTag(updatedTag, User.GetUserId())) - { - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, - MessageFactory.CollectionUpdatedEvent(updatedTag.Id), false); - return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully")); - } + if (await _collectionService.UpdateTag(updatedTag)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully")); } catch (KavitaException ex) { @@ -125,100 +101,18 @@ public class CollectionController : BaseApiController } /// - /// Promote/UnPromote multiple collections in one go. Will only update the authenticated user's collections and will only work if the user has promotion role - /// - /// - /// - [HttpPost("promote-multiple")] - public async Task PromoteMultipleCollections(PromoteCollectionsDto dto) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - // This needs to take into account owner as I can select other users cards - var collections = await _unitOfWork.CollectionTagRepository.GetCollectionsByIds(dto.CollectionIds); - var userId = User.GetUserId(); - - if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) - { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); - } - - foreach (var collection in collections) - { - if (collection.AppUserId != userId) continue; - collection.Promoted = dto.Promoted; - _unitOfWork.CollectionTagRepository.Update(collection); - } - - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - - /// - /// Delete multiple collections in one go - /// - /// - /// - [HttpPost("delete-multiple")] - public async Task DeleteMultipleCollections(DeleteCollectionsDto dto) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections); - if (user == null) return Unauthorized(); - user.Collections = user.Collections.Where(uc => !dto.CollectionIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); - - - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - /// - /// Adds multiple series to a collection. If tag id is 0, this will create a new tag. + /// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag. /// /// /// + [Authorize(Policy = "RequireAdminRole")] [HttpPost("update-for-series")] public async Task AddToMultipleSeries(CollectionTagBulkAddDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - // Create a new tag and save - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections); - if (user == null) return Unauthorized(); + var tag = await _collectionService.GetTagOrCreate(dto.CollectionTagId, dto.CollectionTagTitle); - AppUserCollection? tag; - if (dto.CollectionTagId == 0) - { - tag = new AppUserCollectionBuilder(dto.CollectionTagTitle).Build(); - user.Collections.Add(tag); - } - else - { - // Validate tag doesn't exist - tag = user.Collections.FirstOrDefault(t => t.Id == dto.CollectionTagId); - } - - if (tag == null) - { - return BadRequest(_localizationService.Translate(User.GetUserId(), "collection-doesnt-exists")); - } - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false); - foreach (var s in series) - { - if (tag.Items.Contains(s)) continue; - tag.Items.Add(s); - } - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) return Ok(); + if (await _collectionService.AddTagToSeries(tag, dto.SeriesIds)) return Ok(); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); } @@ -228,14 +122,13 @@ public class CollectionController : BaseApiController /// /// /// + [Authorize(Policy = "RequireAdminRole")] [HttpPost("update-series")] public async Task RemoveTagFromMultipleSeries(UpdateSeriesForTagDto updateSeriesForTagDto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - try { - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(updateSeriesForTagDto.Tag.Id, CollectionIncludes.Series); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata); if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist")); if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) @@ -250,89 +143,27 @@ public class CollectionController : BaseApiController } /// - /// Removes the collection tag from the user + /// Removes the collection tag from all Series it was attached to /// /// /// + [Authorize(Policy = "RequireAdminRole")] [HttpDelete] public async Task DeleteTag(int tagId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - try { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections); - if (user == null) return Unauthorized(); - if (user.Collections.All(c => c.Id != tagId)) - return BadRequest(await _localizationService.Translate(user.Id, "access-denied")); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata); + if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist")); - if (await _collectionService.DeleteTag(tagId, user)) - { + if (await _collectionService.DeleteTag(tag)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-deleted")); - } } - catch (Exception ex) + catch (Exception) { - await _unitOfWork.RollbackAsync(); } return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); } - - /// - /// For the authenticated user, if they have an active Kavita+ subscription and a MAL username on record, - /// fetch their Mal interest stacks (including restacks) - /// - /// - [HttpGet("mal-stacks")] - public async Task>> GetMalStacksForUser() - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - return Ok(await _externalMetadataService.GetStacksForUser(User.GetUserId())); - } - - /// - /// Imports a MAL Stack into Kavita - /// - /// - /// - [HttpPost("import-stack")] - public async Task ImportMalStack(MalStackDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections); - if (user == null) return Unauthorized(); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - - // Validation check to ensure stack doesn't exist already - if (await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id)) - { - return BadRequest(_localizationService.Translate(user.Id, "collection-already-exists")); - } - - try - { - // Create new collection - var newCollection = new AppUserCollectionBuilder(dto.Title) - .WithSource(ScrobbleProvider.Mal) - .WithSourceUrl(dto.Url) - .Build(); - user.Collections.Add(newCollection); - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - // Trigger Stack Refresh for just one stack (not all) - BackgroundJob.Enqueue(() => _collectionSyncService.Sync(newCollection.Id)); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue importing MAL Stack"); - } - - return BadRequest(_localizationService.Translate(user.Id, "error-import-stack")); - } } diff --git a/API/Controllers/ColorScapeController.cs b/API/Controllers/ColorScapeController.cs deleted file mode 100644 index 04827658d..000000000 --- a/API/Controllers/ColorScapeController.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Theme; -using API.Entities.Interfaces; -using API.Extensions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -[Authorize] -public class ColorScapeController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - - public ColorScapeController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - /// - /// Returns the color scape for a series - /// - /// - /// - [HttpGet("series")] - public async Task> GetColorScapeForSeries(int id) - { - var entity = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, User.GetUserId()); - return GetColorSpaceDto(entity); - } - - /// - /// Returns the color scape for a volume - /// - /// - /// - [HttpGet("volume")] - public async Task> GetColorScapeForVolume(int id) - { - var entity = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, User.GetUserId()); - return GetColorSpaceDto(entity); - } - - /// - /// Returns the color scape for a chapter - /// - /// - /// - [HttpGet("chapter")] - public async Task> GetColorScapeForChapter(int id) - { - var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id); - return GetColorSpaceDto(entity); - } - - - private ActionResult GetColorSpaceDto(IHasCoverImage entity) - { - if (entity == null) return Ok(ColorScapeDto.Empty); - return Ok(new ColorScapeDto(entity.PrimaryColor, entity.SecondaryColor)); - } -} diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index 8c8081d98..fa5bc34fa 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -7,14 +7,11 @@ using API.DTOs.Device; using API.Extensions; using API.Services; using API.SignalR; -using AutoMapper; using Kavita.Common; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - /// /// Responsible interacting and creating Devices /// @@ -25,27 +22,20 @@ public class DeviceController : BaseApiController private readonly IEmailService _emailService; private readonly IEventHub _eventHub; private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, - IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService, IMapper mapper) + IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _deviceService = deviceService; _emailService = emailService; _eventHub = eventHub; _localizationService = localizationService; - _mapper = mapper; } - /// - /// Creates a new Device - /// - /// - /// [HttpPost("create")] - public async Task> CreateOrUpdateDevice(CreateDeviceDto dto) + public async Task CreateOrUpdateDevice(CreateDeviceDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices); if (user == null) return Unauthorized(); @@ -54,22 +44,20 @@ public class DeviceController : BaseApiController var device = await _deviceService.Create(dto, user); if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-create")); - - return Ok(_mapper.Map(device)); } catch (KavitaException ex) { return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } + + + + + return Ok(); } - /// - /// Updates an existing Device - /// - /// - /// [HttpPost("update")] - public async Task> UpdateDevice(UpdateDeviceDto dto) + public async Task UpdateDevice(UpdateDeviceDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices); if (user == null) return Unauthorized(); @@ -77,7 +65,7 @@ public class DeviceController : BaseApiController if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-update")); - return Ok(_mapper.Map(device)); + return Ok(); } /// @@ -102,28 +90,18 @@ public class DeviceController : BaseApiController return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId())); } - /// - /// Sends a collection of chapters to the user's device - /// - /// - /// [HttpPost("send-to")] public async Task SendToDevice(SendToDeviceDto dto) { + if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds")); + if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId")); + + if (await _emailService.IsDefaultEmailService()) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); + var userId = User.GetUserId(); - if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(userId, "greater-0", "ChapterIds")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - - // // Validate that the device belongs to the user - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); - if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(userId, "send-to-unallowed")); - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "started"), userId); try { @@ -132,43 +110,38 @@ public class DeviceController : BaseApiController } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); + return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } finally { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), + await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "ended"), userId); } - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to")); } - /// - /// Attempts to send a whole series to a device. - /// - /// - /// + [HttpPost("send-series-to")] public async Task SendSeriesToDevice(SendSeriesToDeviceDto dto) { + if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId")); + if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId")); + + if (await _emailService.IsDefaultEmailService()) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); + var userId = User.GetUserId(); - if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "SeriesId")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "started"), userId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Volumes | SeriesIncludes.Chapters); - if (series == null) return BadRequest(await _localizationService.Translate(userId, "series-doesnt-exist")); + if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist")); var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); try { @@ -177,18 +150,20 @@ public class DeviceController : BaseApiController } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); + return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } finally { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), + await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "ended"), userId); } - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to")); } + + } diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 5a249c9a8..edfda64f6 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using API.Data; using API.DTOs.Downloads; using API.Entities; -using API.Entities.Enums; using API.Extensions; using API.Services; using API.SignalR; @@ -17,8 +16,6 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// All APIs related to downloading entities from the system. Requires Download Role or Admin Role. /// @@ -104,7 +101,7 @@ public class DownloadController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip"); } catch (KavitaException ex) { @@ -119,7 +116,7 @@ public class DownloadController : BaseApiController return await _accountService.HasDownloadPermission(user); } - private PhysicalFileResult GetFirstFileDownload(IEnumerable files) + private ActionResult GetFirstFileDownload(IEnumerable files) { var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true); @@ -141,7 +138,7 @@ public class DownloadController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.Number}.zip"); } catch (KavitaException ex) { @@ -151,43 +148,31 @@ public class DownloadController : BaseApiController private async Task DownloadFiles(ICollection files, string tempFolder, string downloadName) { - var username = User.GetUsername(); - var filename = Path.GetFileNameWithoutExtension(downloadName); try { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, - filename, $"Downloading {filename}", 0F, "started")); - - if (files.Count == 1 && files.First().Format != MangaFormat.Image) + MessageFactory.DownloadProgressEvent(User.GetUsername(), + Path.GetFileNameWithoutExtension(downloadName), 0F, "started")); + if (files.Count == 1) { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, - filename, $"Downloading {filename}",1F, "ended")); + MessageFactory.DownloadProgressEvent(User.GetUsername(), + Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); return GetFirstFileDownload(files); } - var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); - + var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, - filename, "Download Complete", 1F, "ended")); - + MessageFactory.DownloadProgressEvent(User.GetUsername(), + Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true); - - async Task ProgressCallback(Tuple progressInfo) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, filename, $"Processing {Path.GetFileNameWithoutExtension(progressInfo.Item1)}", - Math.Clamp(progressInfo.Item2, 0F, 1F))); - } } catch (Exception ex) { _logger.LogError(ex, "There was an exception when trying to download files"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.DownloadProgressEvent(User.GetUsername(), - filename, "Download Complete", 1F, "ended")); + Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); throw; } } @@ -196,10 +181,8 @@ public class DownloadController : BaseApiController public async Task DownloadSeries(int seriesId) { if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) return BadRequest("Invalid Series"); - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); try { @@ -231,15 +214,15 @@ public class DownloadController : BaseApiController var filename = $"{series!.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}",0F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F)); var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct()); var filePath = _archiveService.CreateZipForDownload(files, $"download_{userId}_{seriesIds}_bookmarks"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 1F)); - return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(filename), true); + return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(filename), true); } } diff --git a/API/Controllers/EmailController.cs b/API/Controllers/EmailController.cs deleted file mode 100644 index c1e3ad413..000000000 --- a/API/Controllers/EmailController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Email; -using API.Helpers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -[Authorize(Policy = "RequireAdminRole")] -public class EmailController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - - public EmailController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - [HttpGet("all")] - public async Task>> GetEmails() - { - return Ok(await _unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default)); - } -} diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 0c925476f..9902d28be 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -5,8 +5,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - [AllowAnonymous] public class FallbackController : Controller { diff --git a/API/Controllers/FilterController.cs b/API/Controllers/FilterController.cs index 7fcffb7da..eeffb10b7 100644 --- a/API/Controllers/FilterController.cs +++ b/API/Controllers/FilterController.cs @@ -10,31 +10,23 @@ using API.DTOs.Filtering.v2; using API.Entities; using API.Extensions; using API.Helpers; -using API.Services; +using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// This is responsible for Filter caching /// public class FilterController : BaseApiController { private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IStreamService _streamService; - private readonly ILogger _logger; + private readonly IEasyCachingProviderFactory _cacheFactory; - public FilterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IStreamService streamService, - ILogger logger) + public FilterController(IUnitOfWork unitOfWork, IEasyCachingProviderFactory cacheFactory) { _unitOfWork = unitOfWork; - _localizationService = localizationService; - _streamService = streamService; - _logger = logger; + _cacheFactory = cacheFactory; } /// @@ -47,7 +39,6 @@ public class FilterController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.SmartFilters); if (user == null) return Unauthorized(); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); if (string.IsNullOrWhiteSpace(dto.Name)) return BadRequest("Name must be set"); if (Seed.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase))) @@ -89,8 +80,6 @@ public class FilterController : BaseApiController [HttpDelete] public async Task DeleteFilter(int filterId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId); if (filter == null) return Ok(); // This needs to delete any dashboard filters that have it too @@ -104,79 +93,4 @@ public class FilterController : BaseApiController await _unitOfWork.CommitAsync(); return Ok(); } - - /// - /// Encode the Filter - /// - /// - /// - [HttpPost("encode")] - public ActionResult EncodeFilter(FilterV2Dto dto) - { - return Ok(SmartFilterHelper.Encode(dto)); - } - - /// - /// Decodes the Filter - /// - /// - /// - [HttpPost("decode")] - public ActionResult DecodeFilter(DecodeFilterDto dto) - { - return Ok(SmartFilterHelper.Decode(dto.EncodedFilter)); - } - - /// - /// Rename a Smart Filter given the filterId and new name - /// - /// - /// - /// - [HttpPost("rename")] - public async Task RenameFilter([FromQuery] int filterId, [FromQuery] string name) - { - try - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), - AppUserIncludes.SmartFilters); - if (user == null) return Unauthorized(); - - name = name.Trim(); - - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) - { - return BadRequest(await _localizationService.Translate(user.Id, "permission-denied")); - } - - if (string.IsNullOrWhiteSpace(name)) - { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-name-required")); - } - - if (Seed.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) - { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-system-name")); - } - - var filter = user.SmartFilters.FirstOrDefault(f => f.Id == filterId); - if (filter == null) - { - return BadRequest(await _localizationService.Translate(user.Id, "filter-not-found")); - } - - filter.Name = name; - _unitOfWork.AppUserSmartFilterRepository.Update(filter); - await _unitOfWork.CommitAsync(); - - await _streamService.RenameSmartFilterStreams(filter); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); - } - - } } diff --git a/API/Controllers/HealthController.cs b/API/Controllers/HealthController.cs index a1931f859..27fe060ea 100644 --- a/API/Controllers/HealthController.cs +++ b/API/Controllers/HealthController.cs @@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - [AllowAnonymous] public class HealthController : BaseApiController { diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 87e0542d1..da484981c 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -7,15 +7,12 @@ using API.Data; using API.Entities.Enums; using API.Extensions; using API.Services; -using API.Services.Tasks.Metadata; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MimeTypes; namespace API.Controllers; -#nullable enable - /// /// Responsible for servicing up images stored in Kavita for entities /// @@ -26,20 +23,15 @@ public class ImageController : BaseApiController private readonly IDirectoryService _directoryService; private readonly IImageService _imageService; private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly ICoverDbService _coverDbService; /// public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - IImageService imageService, ILocalizationService localizationService, - IReadingListService readingListService, ICoverDbService coverDbService) + IImageService imageService, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _directoryService = directoryService; _imageService = imageService; _localizationService = localizationService; - _readingListService = readingListService; - _coverDbService = coverDbService; } /// @@ -66,7 +58,7 @@ public class ImageController : BaseApiController /// /// [HttpGet("library-cover")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["libraryId", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})] public async Task GetLibraryCoverImage(int libraryId, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); @@ -84,7 +76,7 @@ public class ImageController : BaseApiController /// /// [HttpGet("volume-cover")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["volumeId", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})] public async Task GetVolumeCoverImage(int volumeId, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); @@ -101,7 +93,7 @@ public class ImageController : BaseApiController /// /// Id of Series /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["seriesId", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId", "apiKey"})] [HttpGet("series-cover")] public async Task GetSeriesCoverImage(int seriesId, string apiKey) { @@ -117,23 +109,21 @@ public class ImageController : BaseApiController } /// - /// Returns cover image for Collection + /// Returns cover image for Collection Tag /// /// /// [HttpGet("collection-cover")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["collectionTagId", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})] public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { var destFile = await GenerateCollectionCoverImage(collectionTagId); if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); - return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile)); } @@ -148,17 +138,15 @@ public class ImageController : BaseApiController /// /// [HttpGet("readinglist-cover")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["readingListId", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})] public async Task GetReadingListCoverImage(int readingListId, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { - var destFile = await _readingListService.GenerateReadingListCoverImage(readingListId); + var destFile = await GenerateReadingListCoverImage(readingListId); if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile)); } @@ -167,6 +155,22 @@ public class ImageController : BaseApiController return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } + private async Task GenerateReadingListCoverImage(int readingListId) + { + var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); + var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, + ImageService.GetReadingListFormat(readingListId)); + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + destFile += settings.EncodeMediaAs.GetExtension(); + + if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; + ImageService.CreateMergedImage( + covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), + settings.CoverImageSize, + destFile); + return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; + } + private async Task GenerateCollectionCoverImage(int collectionId) { var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); @@ -174,13 +178,11 @@ public class ImageController : BaseApiController ImageService.GetCollectionTagFormat(collectionId)); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); destFile += settings.EncodeMediaAs.GetExtension(); - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; ImageService.CreateMergedImage( covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); - // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; } @@ -193,8 +195,7 @@ public class ImageController : BaseApiController /// API Key for user. Needed to authenticate request /// [HttpGet("bookmark")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["chapterId", "pageNum", "apiKey" - ])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})] public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); @@ -216,7 +217,7 @@ public class ImageController : BaseApiController /// /// [HttpGet("web-link")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["url", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = new []{"url", "apiKey"})] public async Task GetWebLinkImage(string url, string apiKey) { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); @@ -233,7 +234,7 @@ public class ImageController : BaseApiController try { domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, - await _coverDbService.DownloadFaviconAsync(url, encodeFormat)); + await _imageService.DownloadFaviconAsync(url, encodeFormat)); } catch (Exception) { @@ -247,83 +248,6 @@ public class ImageController : BaseApiController return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName)); } - - /// - /// Returns the image associated with a publisher - /// - /// - /// - /// - [HttpGet("publisher")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["publisherName", "apiKey"])] - public async Task GetPublisherImage(string publisherName, string apiKey) - { - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - if (userId == 0) return BadRequest(); - if (string.IsNullOrEmpty(publisherName)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "publisherName")); - if (publisherName.Contains("..")) return BadRequest(); - - var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - // Check if the domain exists - var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); - if (!_directoryService.FileSystem.File.Exists(domainFilePath)) - { - // We need to request the favicon and save it - try - { - domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, - await _coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); - } - catch (Exception) - { - return BadRequest(await _localizationService.Translate(userId, "generic-favicon")); - } - } - - var file = new FileInfo(domainFilePath); - var format = Path.GetExtension(file.FullName); - - return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName)); - } - - /// - /// Returns cover image for Person - /// - /// - /// - [HttpGet("person-cover")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["personId", "apiKey"])] - public async Task GetPersonCoverImage(int personId, string apiKey) - { - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - if (userId == 0) return BadRequest(); - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.PersonRepository.GetCoverImageAsync(personId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); - var format = _directoryService.FileSystem.Path.GetExtension(path); - - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); - } - - /// - /// Returns cover image for Person - /// - /// - /// - [HttpGet("person-cover-by-name")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["personId", "apiKey"])] - public async Task GetPersonCoverImageByName(string name, string apiKey) - { - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - if (userId == 0) return BadRequest(); - - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.PersonRepository.GetCoverImageByNameAsync(name)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image")); - var format = _directoryService.FileSystem.Path.GetExtension(path); - - return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); - } - /// /// Returns a temp coverupload image /// @@ -331,7 +255,7 @@ public class ImageController : BaseApiController /// [Authorize(Policy="RequireAdminRole")] [HttpGet("cover-upload")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["filename", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename", "apiKey"})] public async Task GetCoverUploadImage(string filename, string apiKey) { if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); diff --git a/API/Controllers/KoreaderController.cs b/API/Controllers/KoreaderController.cs deleted file mode 100644 index 8c4c41585..000000000 --- a/API/Controllers/KoreaderController.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Koreader; -using API.Entities; -using API.Extensions; -using API.Services; -using Kavita.Common; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using static System.Net.WebRequestMethods; - -namespace API.Controllers; -#nullable enable - -/// -/// The endpoint to interface with Koreader's Progress Sync plugin. -/// -/// -/// Koreader uses a different form of authentication. It stores the username and password in headers. -/// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua -/// -[AllowAnonymous] -public class KoreaderController : BaseApiController -{ - - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IKoreaderService _koreaderService; - private readonly ILogger _logger; - - public KoreaderController(IUnitOfWork unitOfWork, ILocalizationService localizationService, - IKoreaderService koreaderService, ILogger logger) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _koreaderService = koreaderService; - _logger = logger; - } - - // We won't allow users to be created from Koreader. Rather, they - // must already have an account. - /* - [HttpPost("/users/create")] - public IActionResult CreateUser(CreateUserRequest request) - { - } - */ - - [HttpGet("{apiKey}/users/auth")] - public async Task Authenticate(string apiKey) - { - var userId = await GetUserId(apiKey); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return Unauthorized(); - - return Ok(new { username = user.UserName }); - } - - /// - /// Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible. - /// - /// - /// - /// - [HttpPut("{apiKey}/syncs/progress")] - public async Task> UpdateProgress(string apiKey, KoreaderBookDto request) - { - try - { - var userId = await GetUserId(apiKey); - await _koreaderService.SaveProgress(request, userId); - - return Ok(new KoreaderProgressUpdateDto{ Document = request.Document, Timestamp = DateTime.UtcNow }); - } - catch (KavitaException ex) - { - return BadRequest(ex.Message); - } - } - - /// - /// Gets book progress from Kavita, if not found will return a 400 - /// - /// - /// - /// - [HttpGet("{apiKey}/syncs/progress/{ebookHash}")] - public async Task> GetProgress(string apiKey, string ebookHash) - { - try - { - var userId = await GetUserId(apiKey); - var response = await _koreaderService.GetProgress(ebookHash, userId); - _logger.LogDebug("Koreader response progress for User ({UserId}): {Progress}", userId, response.Progress.Sanitize()); - - return Ok(response); - } - catch (KavitaException ex) - { - return BadRequest(ex.Message); - } - } - - private async Task GetUserId(string apiKey) - { - try - { - return await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - } - catch - { - throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist")); - } - } -} diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 8f9b18317..80cf05009 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -19,17 +19,14 @@ using API.Services.Tasks.Scanner; using API.SignalR; using AutoMapper; using EasyCaching.Core; -using Hangfire; -using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Logging; using TaskScheduler = API.Services.TaskScheduler; namespace API.Controllers; -#nullable enable - [Authorize] public class LibraryController : BaseApiController { @@ -79,21 +76,12 @@ public class LibraryController : BaseApiController .WithFolders(dto.Folders.Select(x => new FolderPath {Path = x}).Distinct().ToList()) .WithFolderWatching(dto.FolderWatching) .WithIncludeInDashboard(dto.IncludeInDashboard) + .WithIncludeInRecommended(dto.IncludeInRecommended) .WithManageCollections(dto.ManageCollections) .WithManageReadingLists(dto.ManageReadingLists) - .WithAllowScrobbling(dto.AllowScrobbling) - .WithAllowMetadataMatching(dto.AllowMetadataMatching) + .WIthAllowScrobbling(dto.AllowScrobbling) .Build(); - library.LibraryFileTypes = dto.FileGroupTypes - .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) - .Distinct() - .ToList(); - library.LibraryExcludePatterns = dto.ExcludePatterns - .Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id}) - .Distinct() - .ToList(); - // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type == LibraryType.Comic) { @@ -113,13 +101,6 @@ public class LibraryController : BaseApiController if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library")); _logger.LogInformation("Created a new library: {LibraryName}", library.Name); - // Restart Folder watching if on - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (settings.EnableFolderWatching) - { - await _libraryWatcher.RestartWatching(); - } - // Assign all the necessary users with this library side nav var userIds = admins.Select(u => u.Id).Append(User.GetUserId()).ToList(); var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) @@ -134,19 +115,13 @@ public class LibraryController : BaseApiController if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library")); - await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - - if (library.FolderWatching) - { - await _libraryWatcher.RestartWatching(); - } - - BackgroundJob.Enqueue(() => _taskScheduler.ScanLibrary(library.Id, false)); + await _libraryWatcher.RestartWatching(); + _taskScheduler.ScanLibrary(library.Id); await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "create"), false); await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(User.GetUserId()), false); - + await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); return Ok(); } @@ -157,7 +132,7 @@ public class LibraryController : BaseApiController /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("list")] - public ActionResult> GetDirectories(string? path) + public ActionResult> GetDirectories(string path) { if (string.IsNullOrEmpty(path)) { @@ -168,40 +143,16 @@ public class LibraryController : BaseApiController })); } - if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path)!)); + if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path))); return Ok(_directoryService.ListDirectory(path)); } - /// - /// Return a specific library - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpGet] - public async Task> GetLibrary(int libraryId) - { - var username = User.GetUsername(); - if (string.IsNullOrEmpty(username)) return Unauthorized(); - - var cacheKey = CacheKey + username; - var result = await _libraryCacheProvider.GetAsync>(cacheKey); - if (result.HasValue) - { - return Ok(result.Value.FirstOrDefault(l => l.Id == libraryId)); - } - - var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username).ToList(); - await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24)); - - return Ok(ret.Find(l => l.Id == libraryId)); - } - /// /// Return all libraries in the Server /// /// - [HttpGet("libraries")] + [HttpGet] public async Task>> GetLibraries() { var username = User.GetUsername(); @@ -213,6 +164,7 @@ public class LibraryController : BaseApiController var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username); await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24)); + _logger.LogDebug("Caching libraries for {Key}", cacheKey); return Ok(ret); } @@ -277,6 +229,8 @@ public class LibraryController : BaseApiController // Bust cache await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); + // TODO: Update a user's SideNav based on library access + _unitOfWork.UserRepository.Update(user); return Ok(_mapper.Map(user)); @@ -297,23 +251,7 @@ public class LibraryController : BaseApiController public async Task Scan(int libraryId, bool force = false) { if (libraryId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "libraryId")); - await _taskScheduler.ScanLibrary(libraryId, force); - return Ok(); - } - - /// - /// Enqueues a bunch of library scans - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("scan-multiple")] - public async Task ScanMultiple(BulkActionDto dto) - { - foreach (var libraryId in dto.Ids) - { - await _taskScheduler.ScanLibrary(libraryId, dto.Force ?? false); - } - + _taskScheduler.ScanLibrary(libraryId, force); return Ok(); } @@ -332,63 +270,17 @@ public class LibraryController : BaseApiController [Authorize(Policy = "RequireAdminRole")] [HttpPost("refresh-metadata")] - public ActionResult RefreshMetadata(int libraryId, bool force = true, bool forceColorscape = true) + public ActionResult RefreshMetadata(int libraryId, bool force = true) { - _taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); + _taskScheduler.RefreshMetadata(libraryId, force); return Ok(); } [Authorize(Policy = "RequireAdminRole")] - [HttpPost("refresh-metadata-multiple")] - public ActionResult RefreshMetadataMultiple(BulkActionDto dto, bool forceColorscape = true) + [HttpPost("analyze")] + public ActionResult Analyze(int libraryId) { - foreach (var libraryId in dto.Ids) - { - _taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape); - } - - return Ok(); - } - - /// - /// Copy the library settings (adv tab + optional type) to a set of other libraries. - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("copy-settings-from")] - public async Task CopySettingsFromLibraryToLibraries(CopySettingsFromLibraryDto dto) - { - var sourceLibrary = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes); - if (sourceLibrary == null) return BadRequest("SourceLibraryId must exist"); - - var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders); - foreach (var targetLibrary in libraries) - { - UpdateLibrarySettings(new UpdateLibraryDto() - { - Folders = targetLibrary.Folders.Select(s => s.Path), - Name = targetLibrary.Name, - Id = targetLibrary.Id, - Type = sourceLibrary.Type, - AllowScrobbling = sourceLibrary.AllowScrobbling, - ExcludePatterns = sourceLibrary.LibraryExcludePatterns.Select(p => p.Pattern).ToList(), - FolderWatching = sourceLibrary.FolderWatching, - ManageCollections = sourceLibrary.ManageCollections, - FileGroupTypes = sourceLibrary.LibraryFileTypes.Select(t => t.FileTypeGroup).ToList(), - IncludeInDashboard = sourceLibrary.IncludeInDashboard, - IncludeInSearch = sourceLibrary.IncludeInSearch, - ManageReadingLists = sourceLibrary.ManageReadingLists - }, targetLibrary, dto.IncludeType); - } - - await _unitOfWork.CommitAsync(); - - if (sourceLibrary.FolderWatching) - { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); - } - + _taskScheduler.AnalyzeFilesForLibrary(libraryId, true); return Ok(); } @@ -418,65 +310,20 @@ public class LibraryController : BaseApiController .Distinct() .Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath); - var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]); + var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, + new List() {dto.FolderPath}); _taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath); return Ok(); } - /// - /// Deletes the library and all series within it. - /// - /// This does not touch any files - /// - /// [Authorize(Policy = "RequireAdminRole")] [HttpDelete("delete")] public async Task> DeleteLibrary(int libraryId) - { - _logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, User.GetUsername()); - - try - { - return Ok(await DeleteLibrary(libraryId, User.GetUserId())); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - - /// - /// Deletes multiple libraries and all series within it. - /// - /// This does not touch any files - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpDelete("delete-multiple")] - public async Task> DeleteMultipleLibraries([FromQuery] List libraryIds) { var username = User.GetUsername(); - _logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", libraryIds, username); - - foreach (var libraryId in libraryIds) - { - try - { - await DeleteLibrary(libraryId, User.GetUserId()); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - - return Ok(); - } - - private async Task DeleteLibrary(int libraryId, int userId) - { + _logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username); var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); var seriesIds = series.Select(x => x.Id).ToArray(); var chapterIds = @@ -487,19 +334,16 @@ public class LibraryController : BaseApiController if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId)) { _logger.LogInformation("User is attempting to delete a library while a scan is in progress"); - throw new KavitaException(await _localizationService.Translate(userId, "delete-library-while-scan")); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan")); } var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); - if (library == null) - { - throw new KavitaException(await _localizationService.Translate(userId, "library-doesnt-exist")); - } - + if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist")); // Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library // Aka SeriesRelation has an invalid foreign key - foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related)) + foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, + SeriesIncludes.Related)) { s.Relations = new List(); _unitOfWork.SeriesRepository.Update(s); @@ -516,7 +360,7 @@ public class LibraryController : BaseApiController await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, - MessageFactory.SideNavUpdateEvent(userId), false); + MessageFactory.SideNavUpdateEvent(User.GetUserId()), false); if (chapterIds.Any()) { @@ -525,7 +369,7 @@ public class LibraryController : BaseApiController _taskScheduler.CleanupChapters(chapterIds); } - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + await _libraryWatcher.RestartWatching(); foreach (var seriesId in seriesIds) { @@ -535,13 +379,13 @@ public class LibraryController : BaseApiController await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false); - return true; + return Ok(true); } catch (Exception ex) { - _logger.LogError(ex, "There was a critical issue. Please try again"); + _logger.LogError(ex, await _localizationService.Translate(User.GetUserId(), "generic-library")); await _unitOfWork.RollbackAsync(); - return false; + return Ok(false); } } @@ -568,40 +412,54 @@ public class LibraryController : BaseApiController [HttpPost("update")] public async Task UpdateLibrary(UpdateLibraryDto dto) { - var userId = User.GetUserId(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); - if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist")); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders); + if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist")); var newName = dto.Name.Trim(); if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) - return BadRequest(await _localizationService.Translate(userId, "library-name-exists")); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists")); - var originalFoldersCount = library.Folders.Count; + var originalFolders = library.Folders.Select(x => x.Path).ToList(); library.Name = newName; library.Folders = dto.Folders.Select(s => new FolderPath() {Path = s}).Distinct().ToList(); var typeUpdate = library.Type != dto.Type; var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching; - UpdateLibrarySettings(dto, library); + library.Type = dto.Type; + library.FolderWatching = dto.FolderWatching; + library.IncludeInDashboard = dto.IncludeInDashboard; + library.IncludeInRecommended = dto.IncludeInRecommended; + library.IncludeInSearch = dto.IncludeInSearch; + library.ManageCollections = dto.ManageCollections; + library.ManageReadingLists = dto.ManageReadingLists; + library.AllowScrobbling = dto.AllowScrobbling; - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update")); - - if (folderWatchingUpdate || originalFoldersCount != dto.Folders.Count() || typeUpdate) + // Override Scrobbling for Comic libraries since there are no providers to scrobble to + if (library.Type == LibraryType.Comic) { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name); + library.AllowScrobbling = false; } - if (originalFoldersCount != dto.Folders.Count() || typeUpdate) + + _unitOfWork.LibraryRepository.Update(library); + + if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library-update")); + if (originalFolders.Count != dto.Folders.Count() || typeUpdate) { - await _taskScheduler.ScanLibrary(library.Id); + await _libraryWatcher.RestartWatching(); + _taskScheduler.ScanLibrary(library.Id); } + if (folderWatchingUpdate) + { + await _libraryWatcher.RestartWatching(); + } await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "update"), false); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, - MessageFactory.SideNavUpdateEvent(userId), false); + MessageFactory.SideNavUpdateEvent(User.GetUserId()), false); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); @@ -609,49 +467,7 @@ public class LibraryController : BaseApiController } - private void UpdateLibrarySettings(UpdateLibraryDto dto, Library library, bool updateType = true) - { - if (updateType) - { - library.Type = dto.Type; - } - library.FolderWatching = dto.FolderWatching; - library.IncludeInDashboard = dto.IncludeInDashboard; - library.IncludeInSearch = dto.IncludeInSearch; - library.ManageCollections = dto.ManageCollections; - library.ManageReadingLists = dto.ManageReadingLists; - library.AllowScrobbling = dto.AllowScrobbling; - library.AllowMetadataMatching = dto.AllowMetadataMatching; - library.EnableMetadata = dto.EnableMetadata; - library.RemovePrefixForSortName = dto.RemovePrefixForSortName; - - library.LibraryFileTypes = dto.FileGroupTypes - .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) - .Distinct() - .ToList(); - - library.LibraryExcludePatterns = dto.ExcludePatterns - .Distinct() - .Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id}) - .ToList(); - - // Override Scrobbling for Comic libraries since there are no providers to scrobble to - if (library.Type is LibraryType.Comic or LibraryType.ComicVine) - { - _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty)); - library.AllowScrobbling = false; - } - - - _unitOfWork.LibraryRepository.Update(library); - } - - /// - /// Returns the type of the underlying library - /// - /// - /// [HttpGet("type")] public async Task> GetLibraryType(int libraryId) { diff --git a/API/Controllers/LicenseController.cs b/API/Controllers/LicenseController.cs index 30ed68771..e02a16b48 100644 --- a/API/Controllers/LicenseController.cs +++ b/API/Controllers/LicenseController.cs @@ -2,31 +2,35 @@ using System.Threading.Tasks; using API.Constants; using API.Data; -using API.DTOs.KavitaPlus.License; +using API.DTOs.Account; +using API.DTOs.License; using API.Entities.Enums; using API.Extensions; using API.Services; using API.Services.Plus; -using EasyCaching.Core; -using Hangfire; +using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using TaskScheduler = API.Services.TaskScheduler; namespace API.Controllers; -#nullable enable - -public class LicenseController( - IUnitOfWork unitOfWork, - ILogger logger, - ILicenseService licenseService, - ILocalizationService localizationService, - ITaskScheduler taskScheduler, - IEasyCachingProviderFactory cachingProviderFactory) - : BaseApiController +public class LicenseController : BaseApiController { + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly ILicenseService _licenseService; + private readonly ILocalizationService _localizationService; + + public LicenseController(IUnitOfWork unitOfWork, ILogger logger, + ILicenseService licenseService, ILocalizationService localizationService) + { + _unitOfWork = unitOfWork; + _logger = logger; + _licenseService = licenseService; + _localizationService = localizationService; + } + /// /// Checks if the user's license is valid or not /// @@ -35,22 +39,11 @@ public class LicenseController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] public async Task> HasValidLicense(bool forceCheck = false) { - - var result = await licenseService.HasActiveLicense(forceCheck); - - var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - var cacheValue = await licenseInfoProvider.GetAsync(LicenseService.CacheKey); - - if (result && !cacheValue.IsNull && !cacheValue.Value) - { - await taskScheduler.ScheduleKavitaPlusTasks(); - } - - return Ok(result); + return Ok(await _licenseService.HasActiveLicense(forceCheck)); } /// - /// Has any license registered with the instance. Does not check Kavita+ API + /// Has any license /// /// [Authorize("RequireAdminRole")] @@ -59,64 +52,22 @@ public class LicenseController( public async Task> HasLicense() { return Ok(!string.IsNullOrEmpty( - (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value)); + (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value)); } - /// - /// Asks Kavita+ for the latest license info - /// - /// Force checking the API and skip the 8 hour cache - /// - [Authorize("RequireAdminRole")] - [HttpGet("info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] - public async Task> GetLicenseInfo(bool forceCheck = false) - { - try - { - return Ok(await licenseService.GetLicenseInfo(forceCheck)); - } - catch (Exception) - { - return Ok(null); - } - } - - /// - /// Remove the Kavita+ License on the Server - /// - /// [Authorize("RequireAdminRole")] [HttpDelete] [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] public async Task RemoveLicense() { - logger.LogInformation("Removing license on file for Server"); - var setting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + _logger.LogInformation("Removing license on file for Server"); + var setting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); setting.Value = null; - unitOfWork.SettingsRepository.Update(setting); - await unitOfWork.CommitAsync(); - - TaskScheduler.RemoveKavitaPlusTasks(); - + _unitOfWork.SettingsRepository.Update(setting); + await _unitOfWork.CommitAsync(); return Ok(); } - - [Authorize("RequireAdminRole")] - [HttpPost("reset")] - public async Task ResetLicense(UpdateLicenseDto dto) - { - logger.LogInformation("Resetting license on file for Server"); - if (await licenseService.ResetLicense(dto.License, dto.Email)) - { - await taskScheduler.ScheduleKavitaPlusTasks(); - return Ok(); - } - - return BadRequest(localizationService.Translate(User.GetUserId(), "unable-to-reset-k+")); - } - /// /// Updates server license /// @@ -128,12 +79,11 @@ public class LicenseController( { try { - await licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim(), dto.DiscordId); - await taskScheduler.ScheduleKavitaPlusTasks(); + await _licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim()); } catch (Exception ex) { - return BadRequest(await localizationService.Translate(User.GetUserId(), ex.Message)); + return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); } return Ok(); } diff --git a/API/Controllers/LocaleController.cs b/API/Controllers/LocaleController.cs index 6e3a2ec78..de1c0d16c 100644 --- a/API/Controllers/LocaleController.cs +++ b/API/Controllers/LocaleController.cs @@ -2,52 +2,47 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.DTOs; using API.DTOs.Filtering; using API.Services; -using EasyCaching.Core; -using Kavita.Common.EnvironmentInfo; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; namespace API.Controllers; -#nullable enable - public class LocaleController : BaseApiController { private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _localeCacheProvider; - private static readonly string CacheKey = "locales_" + BuildInfo.Version; - - public LocaleController(ILocalizationService localizationService, IEasyCachingProviderFactory cachingProviderFactory) + public LocaleController(ILocalizationService localizationService) { _localizationService = localizationService; - _localeCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LocaleOptions); } - /// - /// Returns all applicable locales on the server - /// - /// This can be cached as it will not change per version. - /// - [AllowAnonymous] [HttpGet] - public async Task>> GetAllLocales() + public ActionResult> GetAllLocales() { - var result = await _localeCacheProvider.GetAsync>(CacheKey); - if (result.HasValue) - { - return Ok(result.Value); - } - - var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f); - await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1)); - - return Ok(ret); + var languages = _localizationService.GetLocales().Select(c => + { + try + { + var cult = new CultureInfo(c); + return new LanguageDto() + { + Title = cult.DisplayName, + IsoCode = cult.IetfLanguageTag + }; + } + catch (Exception ex) + { + // Some OS' don't have all culture codes supported like PT_BR, thus we need to default + return new LanguageDto() + { + Title = c, + IsoCode = c + }; + } + }) + .Where(l => !string.IsNullOrEmpty(l.IsoCode)) + .OrderBy(d => d.Title); + return Ok(languages); } } diff --git a/API/Controllers/ManageController.cs b/API/Controllers/ManageController.cs deleted file mode 100644 index 3641ddd74..000000000 --- a/API/Controllers/ManageController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.Services.Plus; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -/// -/// All things centered around Managing the Kavita instance, that isn't aligned with an entity -/// -[Authorize("RequireAdminRole")] -public class ManageController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILicenseService _licenseService; - - public ManageController(IUnitOfWork unitOfWork, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _licenseService = licenseService; - } - - /// - /// Returns a list of all Series that is Kavita+ applicable to metadata match and the status of it - /// - /// - [Authorize("RequireAdminRole")] - [HttpPost("series-metadata")] - public async Task>> SeriesMetadata(ManageMatchFilterDto filter) - { - if (!await _licenseService.HasActiveLicense()) return Ok(Array.Empty()); - - return Ok(await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter)); - } -} diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index cab33692a..0abf032af 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -5,63 +5,45 @@ using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; -using API.Data.Repositories; using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.DTOs.Person; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; using API.Entities.Enums; using API.Extensions; -using API.Helpers; using API.Services; -using API.Services.Plus; using Kavita.Common.Extensions; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable -public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService, - IExternalMetadataService metadataService) - : BaseApiController +public class MetadataController : BaseApiController { - public const string CacheKey = "kavitaPlusSeriesDetail_"; + private readonly IUnitOfWork _unitOfWork; + private readonly ILocalizationService _localizationService; + + public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService) + { + _unitOfWork = unitOfWork; + _localizationService = localizationService; + } /// /// Fetches genres from the instance /// /// String separated libraryIds or null for all genres - /// Context from which this API was invoked /// [HttpGet("genres")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds", "context"])] - public async Task>> GetAllGenres(string? libraryIds, QueryContext context = QueryContext.None) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] + public async Task>> GetAllGenres(string? libraryIds) { - var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .Select(int.Parse) - .ToList(); + var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); + if (ids != null && ids.Count > 0) + { + return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId())); + } - return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context)); - } - - /// - /// Returns a list of Genres with counts for counts when Genre is on Series/Chapter - /// - /// - [HttpPost("genres-with-counts")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] - public async Task>> GetBrowseGenres(UserParams? userParams = null) - { - userParams ??= UserParams.Default; - - var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams); - Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - - return Ok(list); + return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId())); } /// @@ -70,12 +52,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// role /// [HttpGet("people-by-role")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["role"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})] public async Task>> GetAllPeople(PersonRole? role) { return role.HasValue ? - Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role.Value)) : - Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); + Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) : + Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); } /// @@ -84,16 +66,15 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all people /// [HttpGet("people")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllPeople(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids is {Count: > 0}) + if (ids != null && ids.Count > 0) { - return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId(), ids)); + return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId())); } - - return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId())); + return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); } /// @@ -102,31 +83,15 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all tags /// [HttpGet("tags")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllTags(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids is {Count: > 0}) + if (ids != null && ids.Count > 0) { - return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId(), ids)); + return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId())); - } - - /// - /// Returns a list of Tags with counts for counts when Tag is on Series/Chapter - /// - /// - [HttpPost("tags-with-counts")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] - public async Task>> GetBrowseTags(UserParams? userParams = null) - { - userParams ??= UserParams.Default; - - var list = await unitOfWork.TagRepository.GetBrowseableTag(User.GetUserId(), userParams); - Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - - return Ok(list); + return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId())); } /// @@ -135,14 +100,14 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all ratings /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})] [HttpGet("age-ratings")] public async Task>> GetAllAgeRatings(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids is {Count: > 0}) + if (ids != null && ids.Count > 0) { - return Ok(await unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids)); + return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids)); } return Ok(Enum.GetValues().Select(t => new AgeRatingDto() @@ -158,14 +123,14 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all publication status /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})] [HttpGet("publication-status")] public ActionResult> GetAllPublicationStatus(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids is {Count: > 0}) { - return Ok(unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); + return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); } return Ok(Enum.GetValues().Select(t => new PublicationStatusDto() @@ -182,17 +147,14 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all ratings /// [HttpGet("languages")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new []{"libraryIds"})] public async Task>> GetAllLanguages(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - return Ok(await unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids)); + return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids)); } - /// - /// Returns all languages Kavita can accept - /// - /// + [HttpGet("all-languages")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)] public IEnumerable GetAllValidLanguages() @@ -205,78 +167,18 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc }).Where(l => !string.IsNullOrEmpty(l.IsoCode)); } - /// - /// Given a language code returns the display name - /// - /// - /// - [HttpGet("language-title")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["code"])] - public ActionResult GetLanguageTitle(string code) - { - if (string.IsNullOrEmpty(code)) return BadRequest("Code must be provided"); - - return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Where(l => code.Equals(l.IetfLanguageTag)) - .Select(c => c.DisplayName) - .FirstOrDefault(); - } /// - /// If this Series is on Kavita+ Blacklist, removes it. If already cached, invalidates it. - /// This then attempts to refresh data from Kavita+ for this series. + /// Returns summary for the chapter /// - /// + /// /// - // [HttpPost("force-refresh")] - // public async Task ForceRefresh(int seriesId) - // { - // await metadataService.ForceKavitaPlusRefresh(seriesId); - // return Ok(); - // } - - /// - /// Fetches the details needed from Kavita+ for Series Detail page - /// - /// This will hit upstream K+ if the data in local db is 2 weeks old - /// Series Id - /// Library Type - /// - [HttpGet("series-detail-plus")] - public async Task> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType) + [HttpGet("chapter-summary")] + public async Task> GetChapterSummary(int chapterId) { - var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId())) - .Where(r => !string.IsNullOrEmpty(r.Body)) - .OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0) - .ToList(); - - var ret = await metadataService.GetSeriesDetailPlus(seriesId, libraryType); - - await PrepareSeriesDetail(userReviews, ret); - return Ok(ret); - } - - private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto? ret) - { - var isAdmin = User.IsInRole(PolicyConstants.AdminRole); - var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!; - - userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(ret.Reviews.ToList())); - ret.Reviews = userReviews; - - if (!isAdmin && ret.Recommendations != null && user != null) - { - // Re-obtain owned series and take into account age restriction - ret.Recommendations.OwnedSeries = - await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync( - ret.Recommendations.OwnedSeries.Select(s => s.Id), user); - ret.Recommendations.ExternalSeries = []; - } - - if (ret.Recommendations != null && user != null) - { - ret.Recommendations.OwnedSeries ??= []; - await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries); - } + if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + return Ok(chapter.Summary); } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 6e96c3063..14dab489e 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1,34 +1,27 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Xml; using System.Xml.Serialization; using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.OPDS; -using API.DTOs.Person; -using API.DTOs.Progress; using API.DTOs.Search; using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers; using API.Services; -using API.Services.Tasks.Scanner.Parser; -using AutoMapper; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using MimeTypes; namespace API.Controllers; @@ -38,7 +31,6 @@ namespace API.Controllers; [AllowAnonymous] public class OpdsController : BaseApiController { - private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly IDownloadService _downloadService; private readonly IDirectoryService _directoryService; @@ -47,7 +39,6 @@ public class OpdsController : BaseApiController private readonly ISeriesService _seriesService; private readonly IAccountService _accountService; private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; private readonly XmlSerializer _xmlSerializer; @@ -78,14 +69,13 @@ public class OpdsController : BaseApiController }; private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto(); - private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default; + private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default; private const int PageSize = 20; public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService, ICacheService cacheService, IReaderService readerService, ISeriesService seriesService, - IAccountService accountService, ILocalizationService localizationService, - IMapper mapper, ILogger logger) + IAccountService accountService, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _downloadService = downloadService; @@ -95,8 +85,6 @@ public class OpdsController : BaseApiController _seriesService = seriesService; _accountService = accountService; _localizationService = localizationService; - _mapper = mapper; - _logger = logger; _xmlSerializer = new XmlSerializer(typeof(Feed)); _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription)); @@ -111,7 +99,7 @@ public class OpdsController : BaseApiController if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (_, prefix) = await GetPrefix(); + var (baseUrl, prefix) = await GetPrefix(); var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix); SetFeedId(feed, "root"); @@ -153,37 +141,10 @@ public class OpdsController : BaseApiController }); break; case DashboardStreamType.RecentlyUpdated: - feed.Entries.Add(new FeedEntry() - { - Id = "recentlyUpdated", - Title = await _localizationService.Translate(userId, "recently-updated"), - Content = new FeedEntryContent() - { - Text = await _localizationService.Translate(userId, "browse-recently-updated") - }, - Links = new List() - { - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-updated"), - } - }); + // TODO: See if we can implement this and use (count) on series name for number of updates break; case DashboardStreamType.MoreInGenre: - var randomGenre = await _unitOfWork.GenreRepository.GetRandomGenre(); - if (randomGenre == null) break; - - feed.Entries.Add(new FeedEntry() - { - Id = "moreInGenre", - Title = await _localizationService.Translate(userId, "more-in-genre", randomGenre.Title), - Content = new FeedEntryContent() - { - Text = await _localizationService.Translate(userId, "browse-more-in-genre", randomGenre.Title) - }, - Links = new List() - { - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/more-in-genre?genreId={randomGenre.Id}"), - } - }); + // TODO: See if we can implement this break; case DashboardStreamType.SmartFilter: @@ -195,11 +156,10 @@ public class OpdsController : BaseApiController { Text = stream.Name }, - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/smart-filters/{stream.SmartFilterId}/") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{stream.SmartFilterId}/"), + } }); break; } @@ -299,7 +259,7 @@ public class OpdsController : BaseApiController { var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; var prefix = "/api/opds/"; - if (!Configuration.DefaultBaseUrl.Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase)) + if (!Configuration.DefaultBaseUrl.Equals(baseUrl)) { // We need to update the Prefix to account for baseUrl prefix = baseUrl + "api/opds/"; @@ -312,9 +272,9 @@ public class OpdsController : BaseApiController /// Returns the Series matching this smart filter. If FromDashboard, will only return 20 records. /// /// - [HttpGet("{apiKey}/smart-filters/{filterId}")] + [HttpGet("{apiKey}/smart-filter/{filterId}")] [Produces("application/xml")] - public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = 0) + public async Task GetSmartFilter(string apiKey, int filterId) { var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) @@ -324,11 +284,11 @@ public class OpdsController : BaseApiController var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId); if (filter == null) return BadRequest(_localizationService.Translate(userId, "smart-filter-doesnt-exist")); - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix); - SetFeedId(feed, "smartFilters-" + filter.Id); + var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilter-" + filter.Id), $"{prefix}{apiKey}/smart-filter/{filter.Id}/", apiKey, prefix); + SetFeedId(feed, "smartFilter-" + filter.Id); var decodedFilter = SmartFilterHelper.Decode(filter.Filter); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(pageNumber), + var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, UserParams.Default, decodedFilter); var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); @@ -337,7 +297,7 @@ public class OpdsController : BaseApiController feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl)); } - AddPagination(feed, series, $"{prefix}{apiKey}/smart-filters/{filterId}/"); + AddPagination(feed, series, $"{prefix}{apiKey}/smart-filter/{filterId}/"); return CreateXmlResult(SerializeXml(feed)); } @@ -348,23 +308,21 @@ public class OpdsController : BaseApiController var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (_, prefix) = await GetPrefix(); + var (baseUrl, prefix) = await GetPrefix(); var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId); - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); + var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{prefix}{apiKey}/smart-filters", apiKey, prefix); SetFeedId(feed, "smartFilters"); - foreach (var filter in filters) { feed.Entries.Add(new FeedEntry() { Id = filter.Id.ToString(), Title = filter.Name, - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/smart-filters/{filter.Id}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{filter.Id}") + } }); } @@ -379,10 +337,10 @@ public class OpdsController : BaseApiController var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (_, prefix) = await GetPrefix(); + var (baseUrl, prefix) = await GetPrefix(); var externalSources = await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId); - var feed = CreateFeed(await _localizationService.Translate(userId, "external-sources"), $"{apiKey}/external-sources", apiKey, prefix); + var feed = CreateFeed(await _localizationService.Translate(userId, "external-sources"), $"{prefix}{apiKey}/external-sources", apiKey, prefix); SetFeedId(feed, "externalSources"); foreach (var externalSource in externalSources) { @@ -412,26 +370,25 @@ public class OpdsController : BaseApiController if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); var (baseUrl, prefix) = await GetPrefix(); - var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix); + var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId); + var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix); SetFeedId(feed, "libraries"); // Ensure libraries follow SideNav order var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId, false); - foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library).Select(sideNavStream => sideNavStream.Library)) + foreach (var sideNavStream in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library)) { + var library = sideNavStream.Library; feed.Entries.Add(new FeedEntry() { Id = library!.Id.ToString(), Title = library.Name!, - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/libraries/{library.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/libraries/{library.Id}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}") + } }); } @@ -466,31 +423,29 @@ public class OpdsController : BaseApiController var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - + var (baseUrl, prefix) = await GetPrefix(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return Unauthorized(); + var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var tags = await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(user.Id, true); + var tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()) + : (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId)); - var (baseUrl, prefix) = await GetPrefix(); - var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); + + var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{prefix}{apiKey}/collections", apiKey, prefix); SetFeedId(feed, "collections"); - feed.Entries.AddRange(tags.Select(tag => new FeedEntry() { Id = tag.Id.ToString(), Title = tag.Title, Summary = tag.Summary, - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/collections/{tag.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{tag.Id}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}") + } })); return CreateXmlResult(SerializeXml(feed)); @@ -507,9 +462,20 @@ public class OpdsController : BaseApiController var (baseUrl, prefix) = await GetPrefix(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return Unauthorized(); + var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId); - if (tag == null || (tag.AppUserId != user.Id && !tag.Promoted)) + IEnumerable tags; + if (isAdmin) + { + tags = await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync(); + } + else + { + tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId); + } + + var tag = tags.SingleOrDefault(t => t.Id == collectionId); + if (tag == null) { return BadRequest("Collection does not exist or you don't have access"); } @@ -517,7 +483,7 @@ public class OpdsController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(pageNumber)); var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); - var feed = CreateFeed(tag.Title + " Collection", $"{apiKey}/collections/{collectionId}", apiKey, prefix); + var feed = CreateFeed(tag.Title + " Collection", $"{prefix}{apiKey}/collections/{collectionId}", apiKey, prefix); SetFeedId(feed, $"collections-{collectionId}"); AddPagination(feed, series, $"{prefix}{apiKey}/collections/{collectionId}"); @@ -543,10 +509,8 @@ public class OpdsController : BaseApiController true, GetUserParams(pageNumber), false); - var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey, prefix); + var feed = CreateFeed("All Reading Lists", $"{prefix}{apiKey}/reading-list", apiKey, prefix); SetFeedId(feed, "reading-list"); - AddPagination(feed, readingLists, $"{prefix}{apiKey}/reading-list/"); - foreach (var readingListDto in readingLists) { feed.Entries.Add(new FeedEntry() @@ -554,19 +518,15 @@ public class OpdsController : BaseApiController Id = readingListDto.Id.ToString(), Title = readingListDto.Title, Summary = readingListDto.Summary, - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}") + } }); } - return CreateXmlResult(SerializeXml(feed)); } @@ -581,50 +541,31 @@ public class OpdsController : BaseApiController [HttpGet("{apiKey}/reading-list/{readingListId}")] [Produces("application/xml")] - public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = 0) + public async Task GetReadingListItems(int readingListId, string apiKey) { var userId = await GetUser(apiKey); - if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) - { return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - } - + var (baseUrl, prefix) = await GetPrefix(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) - { - return Unauthorized(); - } - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, user.Id); + var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems); + if (userWithLists == null) return Unauthorized(); + var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId); if (readingList == null) { return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted")); } - var (baseUrl, prefix) = await GetPrefix(); - var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); + var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix); SetFeedId(feed, $"reading-list-{readingListId}"); - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId); + var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList(); foreach (var item in items) { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId); - - // If there is only one file underneath, add a direct acquisition link, otherwise add a subsection - if (chapterDto != null && chapterDto.Files.Count == 1) - { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(item.SeriesId, userId); - feed.Entries.Add(await CreateChapterWithFile(userId, item.SeriesId, item.VolumeId, item.ChapterId, - chapterDto.Files.First(), series!, chapterDto, apiKey, prefix, baseUrl)); - } - else - { - feed.Entries.Add( - CreateChapter(apiKey, $"{item.Order} - {item.SeriesName}: {item.Title}", - item.Summary ?? string.Empty, item.ChapterId, item.VolumeId, item.SeriesId, prefix, baseUrl)); - } - + feed.Entries.Add( + CreateChapter(apiKey, $"{item.Order} - {item.SeriesName}: {item.Title}", + string.Empty, item.ChapterId, item.VolumeId, item.SeriesId, prefix, baseUrl)); } return CreateXmlResult(SerializeXml(feed)); } @@ -681,7 +622,7 @@ public class OpdsController : BaseApiController var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(pageNumber), _filterV2Dto); var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id)); - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{apiKey}/recently-added", apiKey, prefix); + var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{prefix}{apiKey}/recently-added", apiKey, prefix); SetFeedId(feed, "recently-added"); AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added"); @@ -693,60 +634,6 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } - [HttpGet("{apiKey}/more-in-genre")] - [Produces("application/xml")] - public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = 1) - { - var userId = await GetUser(apiKey); - if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) - return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (baseUrl, prefix) = await GetPrefix(); - var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId); - var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(pageNumber)); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id)); - - var feed = CreateFeed(await _localizationService.Translate(userId, "more-in-genre", genre.Title), $"{apiKey}/more-in-genre", apiKey, prefix); - SetFeedId(feed, "more-in-genre"); - AddPagination(feed, seriesDtos, $"{prefix}{apiKey}/more-in-genre"); - - foreach (var seriesDto in seriesDtos) - { - feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl)); - } - - return CreateXmlResult(SerializeXml(feed)); - } - - [HttpGet("{apiKey}/recently-updated")] - [Produces("application/xml")] - public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = 1) - { - var userId = await GetUser(apiKey); - if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) - return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); - var (baseUrl, prefix) = await GetPrefix(); - var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, PageSize)).ToList(); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId)); - - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix); - SetFeedId(feed, "recently-updated"); - - foreach (var groupedSeries in seriesDtos) - { - var seriesDto = new SeriesDto() - { - Name = $"{groupedSeries.SeriesName} ({groupedSeries.Count})", - Id = groupedSeries.SeriesId, - Format = groupedSeries.Format, - LibraryId = groupedSeries.LibraryId, - }; - var metadata = seriesMetadatas.First(s => s.SeriesId == seriesDto.Id); - feed.Entries.Add(CreateSeries(seriesDto, metadata, apiKey, prefix, baseUrl)); - } - - return CreateXmlResult(SerializeXml(feed)); - } - [HttpGet("{apiKey}/on-deck")] [Produces("application/xml")] public async Task GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1) @@ -763,7 +650,7 @@ public class OpdsController : BaseApiController Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); - var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{apiKey}/on-deck", apiKey, prefix); + var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{prefix}{apiKey}/on-deck", apiKey, prefix); SetFeedId(feed, "on-deck"); AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck"); @@ -775,12 +662,6 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } - /// - /// OPDS Search endpoint - /// - /// - /// - /// [HttpGet("{apiKey}/series")] [Produces("application/xml")] public async Task SearchSeries(string apiKey, [FromQuery] string query) @@ -798,21 +679,20 @@ public class OpdsController : BaseApiController query = query.Replace(@"%", string.Empty); // Get libraries user has access to var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList(); - if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted")); + if (!libraries.Any()) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted")); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var searchResults = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, - libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false); + var series = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, libraries.Select(l => l.Id).ToArray(), query); - var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey, prefix); + var feed = CreateFeed(query, $"{prefix}{apiKey}/series?query=" + query, apiKey, prefix); SetFeedId(feed, "search-series"); - foreach (var seriesDto in searchResults.Series) + foreach (var seriesDto in series.Series) { feed.Entries.Add(CreateSeries(seriesDto, apiKey, prefix, baseUrl)); } - foreach (var collection in searchResults.Collections) + foreach (var collection in series.Collections) { feed.Entries.Add(new FeedEntry() { @@ -831,7 +711,7 @@ public class OpdsController : BaseApiController }); } - foreach (var readingListDto in searchResults.ReadingLists) + foreach (var readingListDto in series.ReadingLists) { feed.Entries.Add(new FeedEntry() { @@ -845,7 +725,6 @@ public class OpdsController : BaseApiController }); } - // TODO: Search should allow Chapters/Files and more return CreateXmlResult(SerializeXml(feed)); } @@ -890,64 +769,45 @@ public class OpdsController : BaseApiController var (baseUrl, prefix) = await GetPrefix(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - var feed = CreateFeed(series!.Name + " - Storyline", $"{apiKey}/series/{series.Id}", apiKey, prefix); + var feed = CreateFeed(series!.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix); SetFeedId(feed, $"series-{series.Id}"); feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}")); - var chapterDict = new Dictionary(); - var fileDict = new Dictionary(); var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); foreach (var volume in seriesDetail.Volumes) { - var chaptersForVolume = await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id, ChapterIncludes.Files | ChapterIncludes.People); + var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), + _chapterSortComparer); - foreach (var chapter in chaptersForVolume) + foreach (var chapter in chapters) { - var chapterId = chapter.Id; - if (!chapterDict.TryAdd(chapterId, 0)) continue; - - var chapterDto = _mapper.Map(chapter); - foreach (var mangaFile in chapter.Files) + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id); + foreach (var mangaFile in files) { - // If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception - if (!fileDict.TryAdd(mangaFile.Id, 0)) continue; - - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapterId, _mapper.Map(mangaFile), series, - chapterDto, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); } } + } - var chapters = seriesDetail.StorylineChapters; - if (!seriesDetail.StorylineChapters.Any() && seriesDetail.Chapters.Any()) + foreach (var storylineChapter in seriesDetail.StorylineChapters.Where(c => !c.IsSpecial)) { - chapters = seriesDetail.Chapters; - } - - foreach (var chapter in chapters.Where(c => !c.IsSpecial && !chapterDict.ContainsKey(c.Id))) - { - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); - var chapterDto = _mapper.Map(chapter); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(storylineChapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id); foreach (var mangaFile in files) { - // If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception - if (!fileDict.TryAdd(mangaFile.Id, 0)) continue; - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, chapter.VolumeId, chapter.Id, _mapper.Map(mangaFile), series, - chapterDto, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); } } foreach (var special in seriesDetail.Specials) { var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(special.Id); - var chapterDto = _mapper.Map(special); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id); foreach (var mangaFile in files) { - // If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception - if (!fileDict.TryAdd(mangaFile.Id, 0)) continue; - - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, _mapper.Map(mangaFile), series, - chapterDto, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); } } @@ -964,18 +824,20 @@ public class OpdsController : BaseApiController var (baseUrl, prefix) = await GetPrefix(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, VolumeIncludes.Chapters); - + var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); + var chapters = + (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), + _chapterSortComparer); var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ", - $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); + $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s"); - - foreach (var chapter in volume.Chapters) + foreach (var chapter in chapters) { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id, ChapterIncludes.Files | ChapterIncludes.People); - foreach (var mangaFile in chapterDto.Files) + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id); + foreach (var mangaFile in files) { - feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterDto!, apiKey, prefix, baseUrl)); + feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl)); } } @@ -990,20 +852,17 @@ public class OpdsController : BaseApiController if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); var (baseUrl, prefix) = await GetPrefix(); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.People); - + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s", - $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix); + $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix); SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files"); - - foreach (var mangaFile in chapter.Files) + foreach (var mangaFile in files) { feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl)); } @@ -1029,7 +888,7 @@ public class OpdsController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey)); if (!await _accountService.HasDownloadPermission(user)) { - return Forbid("User does not have download permissions"); + return BadRequest("User does not have download permissions"); } var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); @@ -1047,7 +906,7 @@ public class OpdsController : BaseApiController }; } - private static void AddPagination(Feed feed, PagedList list, string href) + private static void AddPagination(Feed feed, PagedList list, string href) { var url = href; if (href.Contains('?')) @@ -1093,21 +952,22 @@ public class OpdsController : BaseApiController Summary = $"Format: {seriesDto.Format}" + (string.IsNullOrWhiteSpace(metadata.Summary) ? string.Empty : $" Summary: {metadata.Summary}"), - Authors = metadata.Writers.Select(CreateAuthor).ToList(), + Authors = metadata.Writers.Select(p => new FeedAuthor() + { + Name = p.Name, + Uri = "http://opds-spec.org/author/" + p.Id + }).ToList(), Categories = metadata.Genres.Select(g => new FeedCategory() { Label = g.Title, Term = string.Empty }).ToList(), - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/series/{seriesDto.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{seriesDto.Id}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}") + } }; } @@ -1118,49 +978,35 @@ public class OpdsController : BaseApiController Id = searchResultDto.SeriesId.ToString(), Title = $"{searchResultDto.Name}", Summary = $"Format: {searchResultDto.Format}", - Links = - [ - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/series/{searchResultDto.SeriesId}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}") - ] + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{searchResultDto.SeriesId}"), + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}") + } }; } - private static FeedAuthor CreateAuthor(PersonDto person) - { - return new FeedAuthor() - { - Name = person.Name, - Uri = "http://opds-spec.org/author/" + person.Id - }; - } - - private static FeedEntry CreateChapter(string apiKey, string title, string? summary, int chapterId, int volumeId, int seriesId, string prefix, string baseUrl) + private static FeedEntry CreateChapter(string apiKey, string title, string summary, int chapterId, int volumeId, int seriesId, string prefix, string baseUrl) { return new FeedEntry() { Id = chapterId.ToString(), Title = title, Summary = summary ?? string.Empty, - - Links = - [ + Links = new List() + { CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"), + $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"), CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}") - ] + } }; } - private async Task CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, - MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl) + private async Task CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl) { var fileSize = mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) : @@ -1169,23 +1015,23 @@ public class OpdsController : BaseApiController var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId); + var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey)); var title = $"{series.Name}"; - if (volume!.Chapters.Count == 1 && !volume.IsSpecial()) + if (volume!.Chapters.Count == 1) { var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); - SeriesService.RenameVolumeName(volume, libraryType, volumeLabel); - if (!volume.IsLooseLeaf()) + SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel); + if (volume.Name != "0") { title += $" - {volume.Name}"; } } - else if (!volume.IsLooseLeaf() && !volume.IsSpecial()) + else if (volume.Number != 0) { - title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}"; + title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}"; } else { @@ -1204,33 +1050,23 @@ public class OpdsController : BaseApiController Id = mangaFile.Id.ToString(), Title = title, Extent = fileSize, - Summary = $"File Type: {fileType.Split("/")[1]} - {fileSize}" + (string.IsNullOrWhiteSpace(chapter.Summary) - ? string.Empty - : $" Summary: {chapter.Summary}"), + Summary = $"{fileType.Split("/")[1]} - {fileSize}", Format = mangaFile.Format.ToString(), - Links = - [ - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), - // We MUST include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly - accLink - ], + Links = new List() + { + CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), + CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"), + // We can't not include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly + accLink, + await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey, prefix) + }, Content = new FeedEntryContent() { Text = fileType, Type = "text" - }, - Authors = chapter.Writers.Select(CreateAuthor).ToList() + } }; - var canPageStream = mangaFile.Extension != ".epub"; - if (canPageStream) - { - entry.Links.Add(await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey, prefix)); - } - return entry; } @@ -1243,15 +1079,14 @@ public class OpdsController : BaseApiController /// /// /// - /// Optional parameter. Can pass false and progress saving will be suppressed /// [HttpGet("{apiKey}/image")] public async Task GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, - [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true) + [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber) { var userId = await GetUser(apiKey); if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page")); - var chapter = await _cacheService.Ensure(chapterId, true); + var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find")); try @@ -1266,9 +1101,11 @@ public class OpdsController : BaseApiController // Calculates SHA1 Hash for byte[] Response.AddCacheHeader(content); - // Save progress for the user (except Panels, they will use a direct connection) + // Save progress for the user + var userAgent = Request.Headers["User-Agent"].ToString(); - if (!userAgent.StartsWith("Panels", StringComparison.InvariantCultureIgnoreCase) || !saveProgress) + Console.WriteLine("User Agent: " + userAgent); + if (!userAgent.Contains("panels", StringComparison.InvariantCultureIgnoreCase)) { await _readerService.SaveReadingProgress(new ProgressDto() { @@ -1277,9 +1114,10 @@ public class OpdsController : BaseApiController SeriesId = seriesId, VolumeId = volumeId, LibraryId =libraryId - }, userId); + }, await GetUser(apiKey)); } + return File(content, MimeTypeMap.GetMimeType(format)); } catch (Exception) @@ -1311,7 +1149,8 @@ public class OpdsController : BaseApiController { try { - return await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var user = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + return user; } catch { @@ -1320,23 +1159,21 @@ public class OpdsController : BaseApiController throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist")); } - private async Task CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFileDto mangaFile, string apiKey, string prefix) + private async Task CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey, string prefix) { var userId = await GetUser(apiKey); var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId); - // NOTE: Type could be wrong, there is nothing I can do in the spec + // TODO: Type could be wrong var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg", $"{prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}"); link.TotalPages = mangaFile.Pages; - link.IsPageStream = true; - if (progress != null) { link.LastRead = progress.PageNum; - link.LastReadDate = progress.LastModifiedUtc.ToString("s"); // Adhere to ISO 8601 + link.LastReadDate = progress.LastModifiedUtc; } - + link.IsPageStream = true; return link; } @@ -1361,61 +1198,20 @@ public class OpdsController : BaseApiController { Title = title, Icon = $"{prefix}{apiKey}/favicon", - Links = - [ + Links = new List() + { link, CreateLink(FeedLinkRelation.Start, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}"), CreateLink(FeedLinkRelation.Search, FeedLinkType.AtomSearch, $"{prefix}{apiKey}/search") - ], + }, }; } - private string SerializeXml(Feed? feed) + private string SerializeXml(Feed feed) { if (feed == null) return string.Empty; - - // Remove invalid XML characters from the feed object - SanitizeFeed(feed); - using var sm = new StringWriter(); _xmlSerializer.Serialize(sm, feed); - - var ret = sm.ToString().Replace("utf-16", "utf-8"); // Chunky cannot accept UTF-16 feeds - - return ret; - } - - // Recursively sanitize all string properties in the object - private static void SanitizeFeed(object? obj) - { - if (obj == null) return; - - var properties = obj.GetType().GetProperties(); - foreach (var property in properties) - { - // Skip properties that require an index (e.g., indexed collections) - if (property.GetIndexParameters().Length > 0) - continue; - - if (property.PropertyType == typeof(string) && property.CanWrite) - { - var value = (string?)property.GetValue(obj); - if (!string.IsNullOrEmpty(value)) - { - property.SetValue(obj, RemoveInvalidXmlChars(value)); - } - } - else if (property.PropertyType.IsClass) // Handle nested objects - { - var nestedObject = property.GetValue(obj); - if (nestedObject != null) - SanitizeFeed(nestedObject); - } - } - } - - private static string RemoveInvalidXmlChars(string input) - { - return new string(input.Where(XmlConvert.IsXmlChar).ToArray()); + return sm.ToString().Replace("utf-16", "utf-8"); // Chunky cannot accept UTF-16 feeds } } diff --git a/API/Controllers/PanelsController.cs b/API/Controllers/PanelsController.cs index d6cdbee2f..809b778f2 100644 --- a/API/Controllers/PanelsController.cs +++ b/API/Controllers/PanelsController.cs @@ -1,15 +1,12 @@ using System.Threading.Tasks; using API.Data; using API.DTOs; -using API.DTOs.Progress; using API.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - /// /// For the Panels app explicitly /// @@ -25,12 +22,6 @@ public class PanelsController : BaseApiController _unitOfWork = unitOfWork; } - /// - /// Saves the progress of a given chapter. - /// - /// - /// - /// [HttpPost("save-progress")] public async Task SaveProgress(ProgressDto dto, [FromQuery] string apiKey) { @@ -39,27 +30,4 @@ public class PanelsController : BaseApiController await _readerService.SaveReadingProgress(dto, userId); return Ok(); } - - /// - /// Gets the Progress of a given chapter - /// - /// - /// - /// The number of pages read, 0 if none read - [HttpGet("get-progress")] - public async Task> GetProgress(int chapterId, [FromQuery] string apiKey) - { - if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required"); - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId); - if (progress == null) return Ok(new ProgressDto() - { - PageNum = 0, - ChapterId = chapterId, - VolumeId = 0, - SeriesId = 0, - }); - return Ok(progress); - } } diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs deleted file mode 100644 index 7328ff954..000000000 --- a/API/Controllers/PersonController.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; -using AutoMapper; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Nager.ArticleNumber; - -namespace API.Controllers; -#nullable enable - -public class PersonController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; - private readonly ICoverDbService _coverDbService; - private readonly IImageService _imageService; - private readonly IEventHub _eventHub; - private readonly IPersonService _personService; - - public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, - ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _mapper = mapper; - _coverDbService = coverDbService; - _imageService = imageService; - _eventHub = eventHub; - _personService = personService; - } - - - [HttpGet] - public async Task> GetPersonByName(string name) - { - return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId())); - } - - /// - /// Find a person by name or alias against a query string - /// - /// - /// - [HttpGet("search")] - public async Task>> SearchPeople([FromQuery] string queryString) - { - return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); - } - - /// - /// Returns all roles for a Person - /// - /// - /// - [HttpGet("roles")] - public async Task>> GetRolesForPersonByName(int personId) - { - return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, User.GetUserId())); - } - - - /// - /// Returns a list of authors and artists for browsing - /// - /// - /// - [HttpPost("all")] - public async Task>> GetPeopleForBrowse(BrowsePersonFilterDto filter, [FromQuery] UserParams? userParams) - { - userParams ??= UserParams.Default; - - var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(User.GetUserId(), filter, userParams); - Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - - return Ok(list); - } - - /// - /// Updates the Person - /// - /// - /// - [Authorize("RequireAdminRole")] - [HttpPost("update")] - public async Task> UpdatePerson(UpdatePersonDto dto) - { - // This needs to get all people and update them equally - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); - if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); - - if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-required")); - - - // Validate the name is unique - if (dto.Name != person.Name && !(await _unitOfWork.PersonRepository.IsNameUnique(dto.Name))) - { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique")); - } - - var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); - if (!success) return BadRequest(await _localizationService.Translate(User.GetUserId(), "aliases-have-overlap")); - - - person.Name = dto.Name?.Trim(); - person.NormalizedName = person.Name.ToNormalized(); - person.Description = dto.Description ?? string.Empty; - person.CoverImageLocked = dto.CoverImageLocked; - - if (dto.MalId is > 0) - { - person.MalId = (long) dto.MalId; - } - if (dto.AniListId is > 0) - { - person.AniListId = (int) dto.AniListId; - } - - if (!string.IsNullOrEmpty(dto.HardcoverId?.Trim())) - { - person.HardcoverId = dto.HardcoverId.Trim(); - } - - var asin = dto.Asin?.Trim(); - if (!string.IsNullOrEmpty(asin) && - (ArticleNumberHelper.IsValidIsbn10(asin) || ArticleNumberHelper.IsValidIsbn13(asin))) - { - person.Asin = asin; - } - - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); - - return Ok(_mapper.Map(person)); - } - - /// - /// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita) - /// - /// - /// - [HttpPost("fetch-cover")] - public async Task> DownloadCoverImage([FromQuery] int personId) - { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var person = await _unitOfWork.PersonRepository.GetPersonById(personId); - if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); - - var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); - - if (string.IsNullOrEmpty(personImage)) - { - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-image-doesnt-exist")); - } - - person.CoverImage = personImage; - _imageService.UpdateColorScape(person); - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); - - return Ok(personImage); - } - - /// - /// Returns the top 20 series that the "person" is known for. This will use Average Rating when applicable (Kavita+ field), else it's a random sort - /// - /// - /// - [HttpGet("series-known-for")] - public async Task>> GetKnownSeries(int personId) - { - return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, User.GetUserId())); - } - - /// - /// Returns all individual chapters by role. Limited to 20 results. - /// - /// - /// - /// - [HttpGet("chapters-by-role")] - public async Task>> GetChaptersByRole(int personId, PersonRole role) - { - return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role)); - } - - /// - /// Merges Persons into one, this action is irreversible - /// - /// - /// - [HttpPost("merge")] - [Authorize("RequireAdminRole")] - public async Task> MergePeople(PersonMergeDto dto) - { - var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); - if (dst == null) return BadRequest(); - - var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); - if (src == null) return BadRequest(); - - await _personService.MergePeopleAsync(src, dst); - await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); - - return Ok(_mapper.Map(dst)); - } - - /// - /// Ensure the alias is valid to be added. For example, the alias cannot be on another person or be the same as the current person name/alias. - /// - /// - /// - /// - [HttpGet("valid-alias")] - public async Task> IsValidAlias(int personId, string alias) - { - var person = await _unitOfWork.PersonRepository.GetPersonById(personId, PersonIncludes.Aliases); - if (person == null) return NotFound(); - - var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(alias); - return Ok(!existingAlias && person.NormalizedName != alias.ToNormalized()); - } - - -} diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index f39462bbf..fd4150349 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using API.Data; using API.DTOs; @@ -12,11 +11,19 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - -public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) - : BaseApiController +public class PluginController : BaseApiController { + private readonly IUnitOfWork _unitOfWork; + private readonly ITokenService _tokenService; + private readonly ILogger _logger; + + public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) + { + _unitOfWork = unitOfWork; + _tokenService = tokenService; + _logger = logger; + } + /// /// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token. /// @@ -30,13 +37,13 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService public async Task> Authenticate([Required] string apiKey, [Required] string pluginName) { // NOTE: In order to log information about plugins, we need some Plugin Description information for each request - // Should log into the access table so we can tell the user + // Should log into access table so we can tell the user var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); - var userAgent = HttpContext.Request.Headers.UserAgent; - var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var userAgent = HttpContext.Request.Headers["User-Agent"]; + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) { - logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new + _logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {Information}", pluginName, new { IpAddress = ipAddress, UserAgent = userAgent, @@ -44,16 +51,15 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService }); throw new KavitaUnauthenticatedUserException(); } - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); - logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({AppUserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); - + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user!.UserName, userId); return new UserDto { Username = user.UserName!, - Token = await tokenService.CreateToken(user), - RefreshToken = await tokenService.CreateRefreshToken(user), + Token = await _tokenService.CreateToken(user), + RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, - KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value + KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } @@ -67,8 +73,8 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService [HttpGet("version")] public async Task> GetVersion([Required] string apiKey) { - var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) throw new KavitaUnauthenticatedUserException(); - return Ok((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value); + return Ok((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value); } } diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index 9283ef6d3..48e609d6b 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -1,102 +1,77 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.Data; -using API.Data.Repositories; using API.DTOs; using API.Extensions; -using API.Services; using API.Services.Plus; +using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// Responsible for providing external ratings for Series /// public class RatingController : BaseApiController { - private readonly IUnitOfWork _unitOfWork; + private readonly ILicenseService _licenseService; private readonly IRatingService _ratingService; - private readonly ILocalizationService _localizationService; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly IEasyCachingProvider _cacheProvider; + public const string CacheKey = "rating_"; - public RatingController(IUnitOfWork unitOfWork, IRatingService ratingService, ILocalizationService localizationService) + public RatingController(ILicenseService licenseService, IRatingService ratingService, + ILogger logger, IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork) { - _unitOfWork = unitOfWork; + _licenseService = licenseService; _ratingService = ratingService; - _localizationService = localizationService; + _logger = logger; + _unitOfWork = unitOfWork; + + _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); } /// - /// Update the users' rating of the given series - /// - /// - /// - /// - [HttpPost("series")] - public async Task UpdateSeriesRating(UpdateRatingDto updateRating) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); - if (user == null) throw new UnauthorizedAccessException(); - - if (await _ratingService.UpdateSeriesRating(user, updateRating)) - { - return Ok(); - } - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); - } - - /// - /// Update the users' rating of the given chapter - /// - /// chapterId must be set - /// - /// - [HttpPost("chapter")] - public async Task UpdateChapterRating(UpdateRatingDto updateRating) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); - if (user == null) throw new UnauthorizedAccessException(); - - if (await _ratingService.UpdateChapterRating(user, updateRating)) - { - return Ok(); - } - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); - } - - /// - /// Overall rating from all Kavita users for a given Series + /// Get the external ratings for a given series /// /// /// - [HttpGet("overall-series")] - public async Task> GetOverallSeriesRating(int seriesId) + [HttpGet] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] + public async Task>> GetRating(int seriesId) + { + + if (!await _licenseService.HasActiveLicense()) + { + return Ok(Enumerable.Empty()); + } + + var cacheKey = CacheKey + seriesId; + var results = await _cacheProvider.GetAsync>(cacheKey); + if (results.HasValue) + { + return Ok(results.Value); + } + + var ratings = await _ratingService.GetRatings(seriesId); + await _cacheProvider.SetAsync(cacheKey, ratings, TimeSpan.FromHours(24)); + _logger.LogDebug("Caching external rating for {Key}", cacheKey); + return Ok(ratings); + } + + [HttpGet("overall")] + public async Task> GetOverallRating(int seriesId) { return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()), - FavoriteCount = 0, - }); - } - - /// - /// Overall rating from all Kavita users for a given Chapter - /// - /// - /// - [HttpGet("overall-chapter")] - public async Task> GetOverallChapterRating(int chapterId) - { - return Ok(new RatingDto() - { - Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, User.GetUserId()), - FavoriteCount = 0, + FavoriteCount = 0 }); } } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 38a5ad482..39748325f 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -7,8 +7,8 @@ using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.Filtering; using API.DTOs.Filtering.v2; -using API.DTOs.Progress; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; @@ -21,12 +21,11 @@ using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using MimeTypes; namespace API.Controllers; -#nullable enable - /// /// For all things regarding reading, mainly focusing on non-Book related entities /// @@ -67,11 +66,11 @@ public class ReaderController : BaseApiController /// /// [HttpGet("pdf")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "apiKey"])] - public async Task GetPdf(int chapterId, string apiKey, bool extractPdf = false) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "apiKey"})] + public async Task GetPdf(int chapterId, string apiKey) { if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return NoContent(); // Validate the user has access to the PDF @@ -89,7 +88,7 @@ public class ReaderController : BaseApiController } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + _cacheService.CleanupChapters(new []{ chapterId }); throw; } } @@ -104,20 +103,18 @@ public class ReaderController : BaseApiController /// Should Kavita extract pdf into images. Defaults to false. /// [HttpGet("image")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "page", "extractPdf", "apiKey" - ])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId","page", "extractPdf", "apiKey"})] [AllowAnonymous] public async Task GetImage(int chapterId, int page, string apiKey, bool extractPdf = false) { if (page < 0) page = 0; var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); + var chapter = await _cacheService.Ensure(chapterId, extractPdf); + if (chapter == null) return NoContent(); try { - var chapter = await _cacheService.Ensure(chapterId, extractPdf); - if (chapter == null) return NoContent(); - var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page)); @@ -127,7 +124,7 @@ public class ReaderController : BaseApiController } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + _cacheService.CleanupChapters(new []{ chapterId }); throw; } } @@ -140,7 +137,7 @@ public class ReaderController : BaseApiController /// /// [HttpGet("thumbnail")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "pageNum", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})] [AllowAnonymous] public async Task GetThumbnail(int chapterId, int pageNum, string apiKey) { @@ -164,14 +161,14 @@ public class ReaderController : BaseApiController /// We must use api key as bookmarks could be leaked to other users via the API /// [HttpGet("bookmark-image")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "page", "apiKey"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"seriesId", "page", "apiKey"})] [AllowAnonymous] public async Task GetBookmarkImage(int seriesId, string apiKey, int page) { + if (page < 0) page = 0; var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return Unauthorized(); - if (page < 0) page = 0; var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId); if (page > totalPages) { @@ -188,7 +185,7 @@ public class ReaderController : BaseApiController } catch (Exception) { - _cacheService.CleanupBookmarks([seriesId]); + _cacheService.CleanupBookmarks(new []{ seriesId }); throw; } } @@ -202,13 +199,12 @@ public class ReaderController : BaseApiController /// /// [HttpGet("file-dimensions")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf"})] public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) { if (chapterId <= 0) return ArraySegment.Empty; var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId))); } @@ -221,8 +217,7 @@ public class ReaderController : BaseApiController /// Include file dimensions. Only useful for image based reading /// [HttpGet("chapter-info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions" - ])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})] public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) { if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore @@ -233,9 +228,6 @@ public class ReaderController : BaseApiController if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan")); var mangaFile = chapter.Files.First(); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, User.GetUserId()); - if (series == null) return Unauthorized(); - var info = new ChapterInfoDto() { ChapterNumber = dto.ChapterNumber, @@ -248,8 +240,6 @@ public class ReaderController : BaseApiController LibraryId = dto.LibraryId, IsSpecial = dto.IsSpecial, Pages = dto.Pages, - SeriesTotalPages = series.Pages, - SeriesTotalPagesRead = series.PagesRead, ChapterTitle = dto.ChapterTitle ?? string.Empty, Subtitle = string.Empty, Title = dto.SeriesName, @@ -262,20 +252,20 @@ public class ReaderController : BaseApiController } if (info.ChapterTitle is {Length: > 0}) { - // TODO: Can we rework the logic of generating titles for the UI and instead calculate that in the DB? info.Title += " - " + info.ChapterTitle; } - if (info.IsSpecial) + if (info.IsSpecial && dto.VolumeNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume)) { - info.Subtitle = Path.GetFileNameWithoutExtension(info.FileName); - } else if (!info.IsSpecial && info.VolumeNumber.Equals(Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)) + info.Subtitle = info.FileName; + } else if (!info.IsSpecial && info.VolumeNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume)) { info.Subtitle = ReaderService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber; } else { - info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber); + //info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber); + info.Subtitle = $"Volume {info.VolumeNumber}"; if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter)) { info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) + @@ -283,7 +273,6 @@ public class ReaderController : BaseApiController } } - return Ok(info); } @@ -294,7 +283,7 @@ public class ReaderController : BaseApiController /// Include file dimensions (extra I/O). Defaults to true. /// [HttpGet("bookmark-info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "includeDimensions"])] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"seriesId", "includeDimensions"})] public async Task> GetBookmarkInfo(int seriesId, bool includeDimensions = true) { var totalPages = await _cacheService.CacheBookmarkForSeries(User.GetUserId(), seriesId); @@ -378,10 +367,13 @@ public class ReaderController : BaseApiController var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress")); + if (await _unitOfWork.CommitAsync()) + { + BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + return Ok(); + } - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); - return Ok(); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress")); } /// @@ -540,8 +532,6 @@ public class ReaderController : BaseApiController public async Task> GetProgress(int chapterId) { var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, User.GetUserId()); - _logger.LogDebug("Get Progress for {ChapterId} is {Pages}", chapterId, progress?.PageNum ?? 0); - if (progress == null) return Ok(new ProgressDto() { PageNum = 0, @@ -553,7 +543,7 @@ public class ReaderController : BaseApiController } /// - /// Save page against Chapter for authenticated user + /// Save page against Chapter for logged in user /// /// /// @@ -750,7 +740,7 @@ public class ReaderController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); if (user == null) return new UnauthorizedResult(); - if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(); + if (user.Bookmarks.IsNullOrEmpty()) return Ok(); if (!await _accountService.HasBookmarkPermission(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission")); @@ -771,7 +761,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("next-chapter")] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { @@ -789,7 +779,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("prev-chapter")] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { @@ -803,7 +793,7 @@ public class ReaderController : BaseApiController /// /// [HttpGet("time-left")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})] public async Task> GetEstimateToCompletion(int seriesId) { var userId = User.GetUserId(); @@ -837,26 +827,16 @@ public class ReaderController : BaseApiController return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId)); } - /// - /// Deletes the user's personal table of content for the given chapter - /// - /// - /// - /// - /// [HttpDelete("ptoc")] public async Task DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title) { var userId = User.GetUserId(); if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); - var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); if (toc == null) return Ok(); - _unitOfWork.UserTableOfContentRepository.Remove(toc); await _unitOfWork.CommitAsync(); - return Ok(); } @@ -892,17 +872,4 @@ public class ReaderController : BaseApiController await _unitOfWork.CommitAsync(); return Ok(); } - - /// - /// Get all progress events for a given chapter - /// - /// - /// - [HttpGet("all-chapter-progress")] - public async Task>> GetProgressForChapter(int chapterId) - { - var userId = User.IsInRole(PolicyConstants.AdminRole) ? 0 : User.GetUserId(); - return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId)); - - } } diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 1187992bc..329fed1e2 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -4,35 +4,31 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs.Person; +using API.DTOs; using API.DTOs.ReadingLists; -using API.Entities.Enums; using API.Extensions; using API.Helpers; using API.Services; +using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - [Authorize] public class ReadingListController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IReadingListService _readingListService; private readonly ILocalizationService _localizationService; - private readonly IReaderService _readerService; public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService, - ILocalizationService localizationService, IReaderService readerService) + ILocalizationService localizationService) { _unitOfWork = unitOfWork; _readingListService = readingListService; _localizationService = localizationService; - _readerService = readerService; } /// @@ -41,15 +37,9 @@ public class ReadingListController : BaseApiController /// /// [HttpGet] - public async Task> GetList(int readingListId) + public async Task>> GetList(int readingListId) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()); - if (readingList == null) - { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-restricted")); - } - - return Ok(readingList); + return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId())); } /// @@ -71,7 +61,7 @@ public class ReadingListController : BaseApiController } /// - /// Returns all Reading Lists the user has access to that the given series within it. + /// Returns all Reading Lists the user has access to that have a series within it. /// /// /// @@ -82,18 +72,6 @@ public class ReadingListController : BaseApiController seriesId, true)); } - /// - /// Returns all Reading Lists the user has access to that has the given chapter within it. - /// - /// - /// - [HttpGet("lists-for-chapter")] - public async Task>> GetListsForChapter(int chapterId) - { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(User.GetUserId(), - chapterId, true)); - } - /// /// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress /// @@ -116,7 +94,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-position")] public async Task UpdateListItemPosition(UpdateReadingListPosition dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); // Make sure UI buffers events var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) @@ -131,14 +108,13 @@ public class ReadingListController : BaseApiController } /// - /// Deletes a list item from the list. Item orders will update as a result. + /// Deletes a list item from the list. Will reorder all item positions afterwards /// /// /// [HttpPost("delete-item")] public async Task DeleteListItem(UpdateReadingListPosition dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -161,8 +137,6 @@ public class ReadingListController : BaseApiController [HttpPost("remove-read")] public async Task DeleteReadFromList([FromQuery] int readingListId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername()); if (user == null) { @@ -174,7 +148,7 @@ public class ReadingListController : BaseApiController return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated")); } - return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-item-delete")); + return BadRequest("Couldn't delete item(s)"); } /// @@ -185,7 +159,6 @@ public class ReadingListController : BaseApiController [HttpDelete] public async Task DeleteList([FromQuery] int readingListId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername()); if (user == null) { @@ -206,7 +179,6 @@ public class ReadingListController : BaseApiController [HttpPost("create")] public async Task> CreateList(CreateReadingListDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingLists); if (user == null) return Unauthorized(); @@ -230,7 +202,6 @@ public class ReadingListController : BaseApiController [HttpPost("update")] public async Task UpdateList(UpdateReadingListDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist")); @@ -260,7 +231,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-by-series")] public async Task UpdateListBySeries(UpdateReadingListBySeriesDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -270,7 +240,7 @@ public class ReadingListController : BaseApiController var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist")); var chapterIdsForSeries = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); + await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId}); // If there are adds, tell tracking this has been modified if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) @@ -303,7 +273,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-by-multiple")] public async Task UpdateListByMultiple(UpdateReadingListByMultipleDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -348,7 +317,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-by-multiple-series")] public async Task UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -387,7 +355,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-by-volume")] public async Task UpdateListByVolume(UpdateReadingListByVolumeDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -424,7 +391,6 @@ public class ReadingListController : BaseApiController [HttpPost("update-by-chapter")] public async Task UpdateListByChapter(UpdateReadingListByChapterDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername()); if (user == null) { @@ -455,38 +421,26 @@ public class ReadingListController : BaseApiController return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); } - /// - /// Returns a list of a given role associated with the reading list - /// - /// - /// PersonRole - /// - [HttpGet("people")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])] - public ActionResult> GetPeopleByRoleForList(int readingListId, PersonRole role) - { - return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role)); - } - - /// - /// Returns all people in given roles for a reading list + /// Returns a list of characters associated with the reading list /// /// /// - [HttpGet("all-people")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])] - public async Task>> GetAllPeopleForList(int readingListId) + [HttpGet("characters")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)] + public ActionResult> GetCharactersForList(int readingListId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId)); + return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId)); } + + /// /// Returns the next chapter within the reading list /// /// /// - /// Chapter ID for next item, -1 if nothing exists + /// Chapter Id for next item, -1 if nothing exists [HttpGet("next-chapter")] public async Task> GetNextChapter(int currentChapterId, int readingListId) { @@ -535,83 +489,4 @@ public class ReadingListController : BaseApiController if (string.IsNullOrEmpty(name)) return true; return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name)); } - - - - /// - /// Promote/UnPromote multiple reading lists in one go. Will only update the authenticated user's reading lists and will only work if the user has promotion role - /// - /// - /// - [HttpPost("promote-multiple")] - public async Task PromoteMultipleReadingLists(PromoteReadingListsDto dto) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - // This needs to take into account owner as I can select other users cards - var userId = User.GetUserId(); - if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) - { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); - } - - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds); - - foreach (var readingList in readingLists) - { - if (readingList.AppUserId != userId) continue; - readingList.Promoted = dto.Promoted; - _unitOfWork.ReadingListRepository.Update(readingList); - } - - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - - /// - /// Delete multiple reading lists in one go - /// - /// - /// - [HttpPost("delete-multiple")] - public async Task DeleteMultipleReadingLists(DeleteReadingListsDto dto) - { - // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ReadingLists); - if (user == null) return Unauthorized(); - - user.ReadingLists = user.ReadingLists.Where(uc => !dto.ReadingListIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); - - - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - /// - /// Returns random information about a Reading List - /// - /// - /// - [HttpGet("info")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])] - public async Task> GetReadingListInfo(int readingListId) - { - var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId); - - if (result == null) return Ok(null); - - var timeEstimate = _readerService.GetTimeEstimate(result.WordCount, result.Pages, result.IsAllEpub); - - result.MinHoursToRead = timeEstimate.MinHours; - result.AvgHoursToRead = timeEstimate.AvgHours; - result.MaxHoursToRead = timeEstimate.MaxHours; - - return Ok(result); - } } diff --git a/API/Controllers/ReadingProfileController.cs b/API/Controllers/ReadingProfileController.cs deleted file mode 100644 index bc1b4fa52..000000000 --- a/API/Controllers/ReadingProfileController.cs +++ /dev/null @@ -1,198 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Extensions; -using API.Services; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace API.Controllers; - -[Route("api/reading-profile")] -public class ReadingProfileController(ILogger logger, IUnitOfWork unitOfWork, - IReadingProfileService readingProfileService): BaseApiController -{ - - /// - /// Gets all non-implicit reading profiles for a user - /// - /// - [HttpGet("all")] - public async Task>> GetAllReadingProfiles() - { - return Ok(await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId(), true)); - } - - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series -> Library -> Default - /// - /// - /// - /// - [HttpGet("{seriesId:int}")] - public async Task> GetProfileForSeries(int seriesId, [FromQuery] bool skipImplicit) - { - return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId, skipImplicit)); - } - - /// - /// Returns the (potential) Reading Profile bound to the library - /// - /// - /// - [HttpGet("library")] - public async Task> GetProfileForLibrary(int libraryId) - { - return Ok(await readingProfileService.GetReadingProfileDtoForLibrary(User.GetUserId(), libraryId)); - } - - /// - /// Creates a new reading profile for the current user - /// - /// - /// - [HttpPost("create")] - public async Task> CreateReadingProfile([FromBody] UserReadingProfileDto dto) - { - return Ok(await readingProfileService.CreateReadingProfile(User.GetUserId(), dto)); - } - - /// - /// Promotes the implicit profile to a user profile. Removes the series from other profiles - /// - /// - /// - [HttpPost("promote")] - public async Task> PromoteImplicitReadingProfile([FromQuery] int profileId) - { - return Ok(await readingProfileService.PromoteImplicitProfile(User.GetUserId(), profileId)); - } - - /// - /// Update the implicit reading profile for a series, creates one if none exists - /// - /// Any modification to the reader settings during reading will create an implicit profile. Use "update-parent" to save to the bound series profile. - /// - /// - /// - [HttpPost("series")] - public async Task> UpdateReadingProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId) - { - var updatedProfile = await readingProfileService.UpdateImplicitReadingProfile(User.GetUserId(), seriesId, dto); - return Ok(updatedProfile); - } - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - [HttpPost("update-parent")] - public async Task> UpdateParentProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId) - { - var newParentProfile = await readingProfileService.UpdateParent(User.GetUserId(), seriesId, dto); - return Ok(newParentProfile); - } - - /// - /// Updates the given reading profile, must belong to the current user - /// - /// - /// The updated reading profile - /// - /// This does not update connected series and libraries. - /// - [HttpPost] - public async Task> UpdateReadingProfile(UserReadingProfileDto dto) - { - return Ok(await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto)); - } - - /// - /// Deletes the given profile, requires the profile to belong to the logged-in user - /// - /// - /// - /// - /// - [HttpDelete] - public async Task DeleteReadingProfile([FromQuery] int profileId) - { - await readingProfileService.DeleteReadingProfile(User.GetUserId(), profileId); - return Ok(); - } - - /// - /// Sets the reading profile for a given series, removes the old one - /// - /// - /// - /// - [HttpPost("series/{seriesId:int}")] - public async Task AddProfileToSeries(int seriesId, [FromQuery] int profileId) - { - await readingProfileService.AddProfileToSeries(User.GetUserId(), profileId, seriesId); - return Ok(); - } - - /// - /// Clears the reading profile for the given series for the currently logged-in user - /// - /// - /// - [HttpDelete("series/{seriesId:int}")] - public async Task ClearSeriesProfile(int seriesId) - { - await readingProfileService.ClearSeriesProfile(User.GetUserId(), seriesId); - return Ok(); - } - - /// - /// Sets the reading profile for a given library, removes the old one - /// - /// - /// - /// - [HttpPost("library/{libraryId:int}")] - public async Task AddProfileToLibrary(int libraryId, [FromQuery] int profileId) - { - await readingProfileService.AddProfileToLibrary(User.GetUserId(), profileId, libraryId); - return Ok(); - } - - /// - /// Clears the reading profile for the given library for the currently logged-in user - /// - /// - /// - /// - [HttpDelete("library/{libraryId:int}")] - public async Task ClearLibraryProfile(int libraryId) - { - await readingProfileService.ClearLibraryProfile(User.GetUserId(), libraryId); - return Ok(); - } - - /// - /// Assigns the reading profile to all passes series, and deletes their implicit profiles - /// - /// - /// - /// - [HttpPost("bulk")] - public async Task BulkAddReadingProfile([FromQuery] int profileId, [FromBody] IList seriesIds) - { - await readingProfileService.BulkAddProfileToSeries(User.GetUserId(), profileId, seriesIds); - return Ok(); - } - -} diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index 259b84fd8..062b87bad 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -1,25 +1,75 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Constants; using API.Data; using API.DTOs; +using API.DTOs.Recommendation; using API.Extensions; using API.Helpers; +using API.Services; +using API.Services.Plus; +using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; namespace API.Controllers; -#nullable enable - public class RecommendedController : BaseApiController { private readonly IUnitOfWork _unitOfWork; - + private readonly IRecommendationService _recommendationService; + private readonly ILicenseService _licenseService; + private readonly ILocalizationService _localizationService; + private readonly IEasyCachingProvider _cacheProvider; public const string CacheKey = "recommendation_"; - public RecommendedController(IUnitOfWork unitOfWork) + public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService, + ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory, + ILocalizationService localizationService) { _unitOfWork = unitOfWork; + _recommendationService = recommendationService; + _licenseService = licenseService; + _localizationService = localizationService; + _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); } + /// + /// For Kavita+ users, this will return recommendations on the server. + /// + /// + /// + [HttpGet("recommendations")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] + public async Task> GetRecommendations(int seriesId) + { + var userId = User.GetUserId(); + if (!await _licenseService.HasActiveLicense()) + { + return Ok(new RecommendationDto()); + } + + if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted")); + } + + var cacheKey = $"{CacheKey}-{seriesId}-{userId}"; + var results = await _cacheProvider.GetAsync(cacheKey); + if (results.HasValue) + { + return Ok(results.Value); + } + + var ret = await _recommendationService.GetRecommendationsForSeries(userId, seriesId); + await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(10)); + return Ok(ret); + } + + /// /// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release. /// @@ -27,7 +77,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("quick-reads")] - public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams) + public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams); @@ -43,7 +93,7 @@ public class RecommendedController : BaseApiController /// /// [HttpGet("quick-catchup-reads")] - public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams) + public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams); @@ -59,7 +109,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("highly-rated")] - public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams? userParams) + public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams) { var userId = User.GetUserId(); userParams ??= UserParams.Default; @@ -77,7 +127,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("more-in")] - public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams? userParams) + public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams) { var userId = User.GetUserId(); @@ -96,7 +146,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("rediscover")] - public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams) + public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams); diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index d4de3db16..50bc55649 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -1,141 +1,156 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; +using API.Services; using API.Services.Plus; using AutoMapper; +using EasyCaching.Core; using Hangfire; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - public class ReviewController : BaseApiController { + private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; + private readonly ILicenseService _licenseService; private readonly IMapper _mapper; + private readonly IReviewService _reviewService; private readonly IScrobblingService _scrobblingService; + private readonly IEasyCachingProvider _cacheProvider; + public const string CacheKey = "review_"; - public ReviewController(IUnitOfWork unitOfWork, - IMapper mapper, IScrobblingService scrobblingService) + public ReviewController(ILogger logger, IUnitOfWork unitOfWork, ILicenseService licenseService, + IMapper mapper, IReviewService reviewService, IScrobblingService scrobblingService, + IEasyCachingProviderFactory cachingProviderFactory) { + _logger = logger; _unitOfWork = unitOfWork; + _licenseService = licenseService; _mapper = mapper; + _reviewService = reviewService; _scrobblingService = scrobblingService; + + _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); } /// - /// Updates the user's review for a given series + /// Fetches reviews from the server for a given series + /// + /// + [HttpGet] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] + public async Task>> GetReviews(int seriesId) + { + var userId = User.GetUserId(); + var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId)) + .Where(r => !string.IsNullOrEmpty(r.Body) && !string.IsNullOrEmpty(r.Tagline)) + .ToList(); + if (!await _licenseService.HasActiveLicense()) + { + return Ok(userRatings); + } + + var cacheKey = CacheKey + seriesId; + IList externalReviews; + + var result = await _cacheProvider.GetAsync>(cacheKey); + if (result.HasValue) + { + externalReviews = result.Value.ToList(); + } + else + { + var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList(); + externalReviews = SelectSpectrumOfReviews(reviews); + + await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10)); + _logger.LogDebug("Caching external reviews for {Key}", cacheKey); + } + + + // Fetch external reviews and splice them in + userRatings.AddRange(externalReviews); + + + return Ok(userRatings); + } + + private static IList SelectSpectrumOfReviews(IList reviews) + { + IList externalReviews; + var totalReviews = reviews.Count; + + if (totalReviews > 10) + { + var stepSize = Math.Max((totalReviews - 4) / 8, 1); + + var selectedReviews = new List() + { + reviews[0], + reviews[1], + }; + for (var i = 2; i < totalReviews - 2; i += stepSize) + { + selectedReviews.Add(reviews[i]); + + if (selectedReviews.Count >= 8) + break; + } + + selectedReviews.Add(reviews[totalReviews - 2]); + selectedReviews.Add(reviews[totalReviews - 1]); + + externalReviews = selectedReviews; + } + else + { + externalReviews = reviews; + } + + return externalReviews; + } + + /// + /// Updates the review for a given series /// /// /// - [HttpPost("series")] - public async Task> UpdateSeriesReview(UpdateUserReviewDto dto) + [HttpPost] + public async Task> UpdateReview(UpdateUserReviewDto dto) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); if (user == null) return Unauthorized(); - var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); + var ratingBuilder = new RatingBuilder(user.Ratings.FirstOrDefault(r => r.SeriesId == dto.SeriesId)); var rating = ratingBuilder .WithBody(dto.Body) .WithSeriesId(dto.SeriesId) - .WithTagline(string.Empty) + .WithTagline(dto.Tagline) .Build(); if (rating.Id == 0) { user.Ratings.Add(rating); } - _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); + BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); + _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, dto.Tagline, dto.Body)); return Ok(_mapper.Map(rating)); } - - /// - /// Update the user's review for a given chapter - /// - /// chapterId must be set - /// - [HttpPost("chapter")] - public async Task> UpdateChapterReview(UpdateUserReviewDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings); - if (user == null) return Unauthorized(); - - if (dto.ChapterId == null) return BadRequest(); - - int chapterId = dto.ChapterId.Value; - - var ratingBuilder = new ChapterRatingBuilder(await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); - - var rating = ratingBuilder - .WithBody(dto.Body) - .WithSeriesId(dto.SeriesId) - .WithChapterId(chapterId) - .Build(); - - if (rating.Id == 0) - { - user.ChapterRatings.Add(rating); - } - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - return Ok(_mapper.Map(rating)); - } - - - /// - /// Deletes the user's review for the given series - /// - /// - [HttpDelete("series")] - public async Task DeleteSeriesReview([FromQuery] int seriesId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); - if (user == null) return Unauthorized(); - - user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - return Ok(); - } - - /// - /// Deletes the user's review for the given chapter - /// - /// - [HttpDelete("chapter")] - public async Task DeleteChapterReview([FromQuery] int chapterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings); - if (user == null) return Unauthorized(); - - user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList(); - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - return Ok(); - } } diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index 986f4f8e7..d81267936 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs.Account; -using API.DTOs.KavitaPlus.Account; using API.DTOs.Scrobbling; using API.Entities.Scrobble; using API.Extensions; @@ -22,7 +21,6 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable public class ScrobblingController : BaseApiController { @@ -40,44 +38,22 @@ public class ScrobblingController : BaseApiController _localizationService = localizationService; } - /// - /// Get the current user's AniList token - /// - /// [HttpGet("anilist-token")] - public async Task> GetAniListToken() + public async Task GetAniListToken() { + // Validate the license + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); return Ok(user.AniListAccessToken); } - /// - /// Get the current user's MAL token and username - /// - /// - [HttpGet("mal-token")] - public async Task> GetMalToken() - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (user == null) return Unauthorized(); - - return Ok(new MalUserInfoDto() - { - Username = user.MalUserName, - AccessToken = user.MalAccessToken - }); - } - - /// - /// Update the current user's AniList token - /// - /// - /// True if the token was new or not [HttpPost("update-anilist-token")] - public async Task> UpdateAniListToken(AniListUpdateDto dto) + public async Task UpdateAniListToken(AniListUpdateDto dto) { + // Validate the license + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); @@ -86,47 +62,14 @@ public class ScrobblingController : BaseApiController _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); - return Ok(isNewToken); - } - - /// - /// Update the current user's MAL token (Client ID) and Username - /// - /// - /// True if the token was new or not - [HttpPost("update-mal-token")] - public async Task> UpdateMalToken(MalUserInfoDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (user == null) return Unauthorized(); - - var isNewToken = string.IsNullOrEmpty(user.MalAccessToken); - user.MalAccessToken = dto.AccessToken; - user.MalUserName = dto.Username; - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - return Ok(isNewToken); - } - - /// - /// When a user request to generate scrobble events from history. Should only be ran once per user. - /// - /// - [HttpPost("generate-scrobble-events")] - public ActionResult GenerateScrobbleEvents() - { - BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(User.GetUserId())); + if (isNewToken) + { + BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(user.Id)); + } return Ok(); } - /// - /// Checks if the current Scrobbling token for the given Provider has expired for the current user - /// - /// - /// [HttpGet("token-expired")] public async Task> HasTokenExpired(ScrobbleProvider provider) { @@ -215,20 +158,15 @@ public class ScrobblingController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) - return Ok(await _localizationService.Translate(user.Id, "nothing-to-do")); + return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); - var seriesHold = new ScrobbleHoldBuilder() - .WithSeriesId(seriesId) - .Build(); + var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build(); user.ScrobbleHolds.Add(seriesHold); _unitOfWork.UserRepository.Update(user); try { _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); - - // When a hold is placed on a series, clear any pre-existing Scrobble Events - await _scrobblingService.ClearEventsForSeries(user.Id, seriesId); return Ok(); } catch (DbUpdateConcurrencyException ex) @@ -254,7 +192,7 @@ public class ScrobblingController : BaseApiController } /// - /// Remove a hold against the Series for user's scrobbling + /// Adds a hold against the Series for user's scrobbling /// /// /// @@ -270,29 +208,4 @@ public class ScrobblingController : BaseApiController await _unitOfWork.CommitAsync(); return Ok(); } - - /// - /// Has the logged in user ran scrobble generation - /// - /// - [HttpGet("has-ran-scrobble-gen")] - public async Task> HasRanScrobbleGen() - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); - return Ok(user is {HasRunScrobbleEventGeneration: true}); - } - - /// - /// Delete the given scrobble events if they belong to that user - /// - /// - /// - [HttpPost("bulk-remove-events")] - public async Task BulkRemoveScrobbleEvents(IList eventIds) - { - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(User.GetUserId(), eventIds); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); - return Ok(); - } } diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index cc89a124e..98c969800 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -10,8 +10,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - /// /// Responsible for the Search interface from the UI /// @@ -50,27 +48,20 @@ public class SearchController : BaseApiController return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, User.GetUserId())); } - /// - /// Searches against different entities in the system against a query string - /// - /// - /// Include Chapter and Filenames in the entities. This can slow down the search on larger systems - /// [HttpGet("search")] - public async Task> Search(string queryString, [FromQuery] bool includeChapterAndFiles = true) + public async Task> Search(string queryString) { queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); - var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList(); - if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted")); + if (!libraries.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted")); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, - libraries, queryString, includeChapterAndFiles); + libraries, queryString); return Ok(series); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 389ff33a7..eeb52e89f 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; @@ -9,30 +10,24 @@ using API.DTOs.Dashboard; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.Metadata; -using API.DTOs.Metadata.Matching; using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.MetadataMatching; using API.Extensions; using API.Helpers; using API.Services; using API.Services.Plus; using EasyCaching.Core; -using Hangfire; using Kavita.Common; using Kavita.Common.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - public class SeriesController : BaseApiController { private readonly ILogger _logger; @@ -42,17 +37,16 @@ public class SeriesController : BaseApiController private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly IExternalMetadataService _externalMetadataService; - private readonly IHostEnvironment _environment; + private readonly IEasyCachingProvider _ratingCacheProvider; + private readonly IEasyCachingProvider _reviewCacheProvider; + private readonly IEasyCachingProvider _recommendationCacheProvider; private readonly IEasyCachingProvider _externalSeriesCacheProvider; - private readonly IEasyCachingProvider _matchSeriesCacheProvider; - private const string CacheKey = "externalSeriesData_"; - private const string MatchSeriesCacheKey = "matchSeries_"; + private const string CacheKey = "recommendation_"; public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService, ILicenseService licenseService, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, - IExternalMetadataService externalMetadataService, IHostEnvironment environment) + IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, IExternalMetadataService externalMetadataService) { _logger = logger; _taskScheduler = taskScheduler; @@ -61,10 +55,11 @@ public class SeriesController : BaseApiController _licenseService = licenseService; _localizationService = localizationService; _externalMetadataService = externalMetadataService; - _environment = environment; + _ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); + _reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); + _recommendationCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); - _matchSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusMatchSeries); } /// @@ -100,7 +95,7 @@ public class SeriesController : BaseApiController /// /// [HttpPost("v2")] - public async Task>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto) + public async Task>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto) { var userId = User.GetUserId(); var series = @@ -131,11 +126,6 @@ public class SeriesController : BaseApiController return Ok(series); } - /// - /// Deletes a series from Kavita - /// - /// - /// If the series was deleted or not [Authorize(Policy = "RequireAdminRole")] [HttpDelete("{seriesId}")] public async Task> DeleteSeries(int seriesId) @@ -143,7 +133,7 @@ public class SeriesController : BaseApiController var username = User.GetUsername(); _logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); - return Ok(await _seriesService.DeleteMultipleSeries([seriesId])); + return Ok(await _seriesService.DeleteMultipleSeries(new[] {seriesId})); } [Authorize(Policy = "RequireAdminRole")] @@ -151,9 +141,9 @@ public class SeriesController : BaseApiController public async Task DeleteMultipleSeries(DeleteSeriesDto dto) { var username = User.GetUsername(); - _logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); + _logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); - if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); + if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete")); } @@ -185,13 +175,27 @@ public class SeriesController : BaseApiController return Ok(await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter)); } - [Obsolete("All chapter entities will load this data by default. Will not be maintained as of v0.8.1")] [HttpGet("chapter-metadata")] public async Task> GetChapterMetadata(int chapterId) { return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); } + + /// + /// Update the user rating for the given series + /// + /// + /// + [HttpPost("update-rating")] + public async Task UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings); + if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); + return Ok(); + } + /// /// Updates the Series /// @@ -223,27 +227,22 @@ public class SeriesController : BaseApiController { // Trigger a refresh when we are moving from a locked image to a non-locked needsRefreshMetadata = true; - series.CoverImage = null; - series.CoverImageLocked = false; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); - series.ResetColorScape(); - + series.CoverImage = string.Empty; + series.CoverImageLocked = updateSeries.CoverImageLocked; } _unitOfWork.SeriesRepository.Update(series); - if (!await _unitOfWork.CommitAsync()) + if (await _unitOfWork.CommitAsync()) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-update")); + if (needsRefreshMetadata) + { + _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); + } + return Ok(); } - if (needsRefreshMetadata) - { - _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); - } - - return Ok(); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-update")); } /// @@ -312,17 +311,18 @@ public class SeriesController : BaseApiController /// /// /// - /// This is not in use + /// /// [HttpPost("all-v2")] - public async Task>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, - [FromQuery] int libraryId = 0, [FromQuery] QueryContext context = QueryContext.None) + public async Task>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { var userId = User.GetUserId(); var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context); + await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); // Apply progress/rating information (I can't work out how to do this in initial query) + if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series")); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -338,7 +338,7 @@ public class SeriesController : BaseApiController /// /// [HttpPost("all")] - [Obsolete("Use all-v2")] + [Obsolete("User all-v2")] public async Task>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { var userId = User.GetUserId(); @@ -397,7 +397,7 @@ public class SeriesController : BaseApiController [HttpPost("refresh-metadata")] public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); + _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); return Ok(); } @@ -435,7 +435,8 @@ public class SeriesController : BaseApiController [HttpGet("metadata")] public async Task> GetSeriesMetadata(int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId)); + var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId); + return Ok(metadata); } /// @@ -449,6 +450,19 @@ public class SeriesController : BaseApiController if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail")); + if (await _licenseService.HasActiveLicense()) + { + _logger.LogDebug("Clearing cache as series weblinks may have changed"); + await _reviewCacheProvider.RemoveAsync(ReviewController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); + await _ratingCacheProvider.RemoveAsync(RatingController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); + + var allUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(s => s.Id); + foreach (var userId in allUsers) + { + await _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}"); + } + } + return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated")); } @@ -494,7 +508,7 @@ public class SeriesController : BaseApiController /// /// /// This is cached for an hour - [ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = ["ageRating"])] + [ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = new [] {"ageRating"})] [HttpGet("age-rating")] public async Task> GetAgeRating(int ageRating) { @@ -590,7 +604,7 @@ public class SeriesController : BaseApiController await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15)); return Ok(ret); } - catch (Exception) + catch (Exception ex) { return BadRequest("Unable to load External Series details"); } @@ -610,52 +624,4 @@ public class SeriesController : BaseApiController return Ok(await _seriesService.GetEstimatedChapterCreationDate(seriesId, userId)); } - /// - /// Sends a request to Kavita+ API for all potential matches, sorted by relevance - /// - /// - /// - [HttpPost("match")] - public async Task>> MatchSeries(MatchSeriesDto dto) - { - var cacheKey = $"{MatchSeriesCacheKey}-{dto.SeriesId}-{dto.Query}"; - var results = await _matchSeriesCacheProvider.GetAsync>(cacheKey); - if (results.HasValue && !_environment.IsDevelopment()) - { - return Ok(results.Value); - } - - var ret = await _externalMetadataService.MatchSeries(dto); - await _matchSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(1)); - - return Ok(ret); - } - - /// - /// This will perform the fix match - /// - /// - /// - /// - [HttpPost("update-match")] - public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId) - { - BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); - - return Ok(); - } - - /// - /// When true, will not perform a match and will prevent Kavita from attempting to match/scrobble against this series - /// - /// - /// - /// - [HttpPost("dont-match")] - public async Task UpdateDontMatch([FromQuery] int seriesId, [FromQuery] bool dontMatch) - { - await _externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch); - return Ok(); - } - } diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 79f6391e8..a770fbc6e 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -20,14 +20,13 @@ using Hangfire.Storage; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using MimeTypes; using TaskScheduler = API.Services.TaskScheduler; namespace API.Controllers; -#nullable enable - [Authorize(Policy = "RequireAdminRole")] public class ServerController : BaseApiController { @@ -38,16 +37,18 @@ public class ServerController : BaseApiController private readonly IStatsService _statsService; private readonly ICleanupService _cleanupService; private readonly IScannerService _scannerService; + private readonly IAccountService _accountService; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; private readonly IEasyCachingProviderFactory _cachingProviderFactory; private readonly ILocalizationService _localizationService; + private readonly IEmailService _emailService; public ServerController(ILogger logger, - IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, - IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, + IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, + ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService) + ILocalizationService localizationService, IEmailService emailService) { _logger = logger; _backupService = backupService; @@ -56,10 +57,12 @@ public class ServerController : BaseApiController _statsService = statsService; _cleanupService = cleanupService; _scannerService = scannerService; + _accountService = accountService; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; _cachingProviderFactory = cachingProviderFactory; _localizationService = localizationService; + _emailService = emailService; } /// @@ -88,19 +91,6 @@ public class ServerController : BaseApiController return Ok(); } - /// - /// Performs the nightly maintenance work on the Server. Can be heavy. - /// - /// - [HttpPost("cleanup")] - public ActionResult Cleanup() - { - _logger.LogInformation("{UserName} is clearing running general cleanup from admin dashboard", User.GetUsername()); - RecurringJob.TriggerJob(TaskScheduler.CleanupTaskId); - - return Ok(); - } - /// /// Performs an ad-hoc backup of the Database /// @@ -129,6 +119,15 @@ public class ServerController : BaseApiController return Ok(); } + /// + /// Returns non-sensitive information about the current system + /// + /// + [HttpGet("server-info")] + public async Task> GetVersion() + { + return Ok(await _statsService.GetServerInfo()); + } /// /// Returns non-sensitive information about the current system @@ -136,7 +135,7 @@ public class ServerController : BaseApiController /// This is just for the UI and is extremely lightweight /// [HttpGet("server-info-slim")] - public async Task> GetSlimVersion() + public async Task> GetSlimVersion() { return Ok(await _statsService.GetServerInfoSlim()); } @@ -180,50 +179,36 @@ public class ServerController : BaseApiController } } - /// - /// Checks for updates and pushes an event to the UI - /// - /// Some users have websocket issues so this is not always reliable to alert the user - [HttpGet("check-for-updates")] - public async Task CheckForAnnouncements() - { - await _taskScheduler.CheckForUpdate(); - return Ok(); - } - /// /// Checks for updates, if no updates that are > current version installed, returns null /// [HttpGet("check-update")] - public async Task> CheckForUpdates() + public async Task> CheckForUpdates() { return Ok(await _versionUpdaterService.CheckForUpdate()); } - /// - /// Returns how many versions out of date this install is - /// - /// Only count Stable releases - [HttpGet("check-out-of-date")] - public async Task> CheckHowOutOfDate(bool stableOnly = true) - { - return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind(stableOnly)); - } - /// /// Pull the Changelog for Kavita from Github and display /// - /// How many releases from the latest to return /// - [AllowAnonymous] [HttpGet("changelog")] - public async Task>> GetChangelog(int count = 0) + public async Task>> GetChangelog() { - // Strange bug where [Authorize] doesn't work - if (User.GetUserId() == 0) return Unauthorized(); + return Ok(await _versionUpdaterService.GetAllReleases()); + } - return Ok(await _versionUpdaterService.GetAllReleases(count)); + /// + /// Is this server accessible to the outside net + /// + /// If the instance has the HostName set, this will return true whether or not it is accessible externally + /// + [HttpGet("accessible")] + [AllowAnonymous] + public async Task> IsServerAccessible() + { + return Ok(await _accountService.CheckIfAccessible(Request)); } /// @@ -231,18 +216,18 @@ public class ServerController : BaseApiController /// /// [HttpGet("jobs")] - public async Task>> GetJobs() + public ActionResult> GetJobs() { - var jobDtoTasks = JobStorage.Current.GetConnection().GetRecurringJobs().Select(async dto => - new JobDto() - { - Id = dto.Id, - Title = await _localizationService.Translate(User.GetUserId(), dto.Id), - Cron = dto.Cron, - LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null - }); + var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs().Select( + dto => + new JobDto() { + Id = dto.Id, + Title = dto.Id.Replace('-', ' '), + Cron = dto.Cron, + LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null + }); - return Ok(await Task.WhenAll(jobDtoTasks)); + return Ok(recurringJobs); } /// @@ -274,25 +259,35 @@ public class ServerController : BaseApiController /// /// [Authorize("RequireAdminRole")] - [HttpPost("bust-kavitaplus-cache")] + [HttpPost("bust-review-and-rec-cache")] public async Task BustReviewAndRecCache() { _logger.LogInformation("Busting Kavita+ Cache"); - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); + await provider.FlushAsync(); + provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); + await provider.FlushAsync(); + provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); await provider.FlushAsync(); return Ok(); } /// - /// Runs the Sync Themes task + /// Returns the KavitaEmail version for non-default instances /// /// [Authorize("RequireAdminRole")] - [HttpPost("sync-themes")] - public async Task SyncThemes() + [HttpGet("email-version")] + public async Task> GetEmailVersion() { - await _taskScheduler.SyncThemes(); - return Ok(); + var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)) + .Value; + + if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null); + + return Ok(await _emailService.GetVersion(emailServiceUrl)); + } + } diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 0610c8705..c5b368fbc 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -5,9 +5,7 @@ using System.Net; using System.Threading.Tasks; using API.Data; using API.DTOs.Email; -using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Settings; -using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Converters; @@ -15,8 +13,7 @@ using API.Logging; using API.Services; using API.Services.Tasks.Scanner; using AutoMapper; -using Cronos; -using Hangfire; +using Flurl.Http; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Extensions; @@ -24,36 +21,34 @@ using Kavita.Common.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Swashbuckle.AspNetCore.Annotations; namespace API.Controllers; -#nullable enable - public class SettingsController : BaseApiController { private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; + private readonly ITaskScheduler _taskScheduler; + private readonly IDirectoryService _directoryService; private readonly IMapper _mapper; private readonly IEmailService _emailService; + private readonly ILibraryWatcher _libraryWatcher; private readonly ILocalizationService _localizationService; - private readonly ISettingsService _settingsService; - public SettingsController(ILogger logger, IUnitOfWork unitOfWork, IMapper mapper, - IEmailService emailService, ILocalizationService localizationService, ISettingsService settingsService) + public SettingsController(ILogger logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, + IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher, + ILocalizationService localizationService) { _logger = logger; _unitOfWork = unitOfWork; + _taskScheduler = taskScheduler; + _directoryService = directoryService; _mapper = mapper; _emailService = emailService; + _libraryWatcher = libraryWatcher; _localizationService = localizationService; - _settingsService = settingsService; } - /// - /// Returns the base url for this instance (if set) - /// - /// [HttpGet("base-url")] public async Task> GetBaseUrl() { @@ -61,10 +56,6 @@ public class SettingsController : BaseApiController return Ok(settingsDto.BaseUrl); } - /// - /// Returns the server settings - /// - /// [Authorize(Policy = "RequireAdminRole")] [HttpGet] public async Task> GetSettings() @@ -126,49 +117,279 @@ public class SettingsController : BaseApiController } /// - /// Is the minimum information setup for Email to work + /// Resets the email service url /// /// [Authorize(Policy = "RequireAdminRole")] - [HttpGet("is-email-setup")] - public async Task> IsEmailSetup() + [HttpPost("reset-email-url")] + public async Task> ResetEmailServiceUrlSettings() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - return Ok(settings.IsEmailSetup()); + _logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername()); + var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl); + emailSetting.Value = EmailService.DefaultApiUrl; + _unitOfWork.SettingsRepository.Update(emailSetting); + + if (!await _unitOfWork.CommitAsync()) + { + await _unitOfWork.RollbackAsync(); + } + + return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); + } + + /// + /// Sends a test email from the Email Service. Will not send if email service is the Default Provider + /// + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("test-email-url")] + public async Task> TestEmailServiceUrl(TestEmailDto dto) + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); + var emailService = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + return Ok(await _emailService.TestConnectivity(dto.Url, user!.Email, !emailService.Equals(EmailService.DefaultApiUrl))); } - /// - /// Update Server settings - /// - /// - /// + [Authorize(Policy = "RequireAdminRole")] [HttpPost] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); + // We do not allow CacheDirectory changes, so we will ignore. + var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); + var updateBookmarks = false; + var originalBookmarkDirectory = _directoryService.BookmarkDirectory; + + var bookmarkDirectory = updateSettingsDto.BookmarksDirectory; + if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") && + !updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/")) + { + bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); + } + + if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory)) + { + bookmarkDirectory = _directoryService.BookmarkDirectory; + } + + foreach (var setting in currentSettings) + { + if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) + { + setting.Value = updateSettingsDto.TaskBackup; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) + { + setting.Value = updateSettingsDto.TaskScan; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.CoverImageSize && updateSettingsDto.CoverImageSize + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.CoverImageSize + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) + { + setting.Value = updateSettingsDto.TaskScan; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) + { + if (OsInfo.IsDocker) continue; + setting.Value = updateSettingsDto.Port + string.Empty; + // Port is managed in appSetting.json + Configuration.Port = updateSettingsDto.Port; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.CacheSize && updateSettingsDto.CacheSize + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.CacheSize + string.Empty; + // CacheSize is managed in appSetting.json + Configuration.CacheSize = updateSettingsDto.CacheSize; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value) + { + if (OsInfo.IsDocker) continue; + // Validate IP addresses + foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + if (!IPAddress.TryParse(ipAddress.Trim(), out _)) { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress)); + } + } + + setting.Value = updateSettingsDto.IpAddresses; + // IpAddresses is managed in appSetting.json + Configuration.IpAddresses = updateSettingsDto.IpAddresses; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) + { + var path = !updateSettingsDto.BaseUrl.StartsWith('/') + ? $"/{updateSettingsDto.BaseUrl}" + : updateSettingsDto.BaseUrl; + path = !path.EndsWith('/') + ? $"{path}/" + : path; + setting.Value = path; + Configuration.BaseUrl = updateSettingsDto.BaseUrl; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.LoggingLevel + string.Empty; + LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel); + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.EnableOpds + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EncodeMediaAs && updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.EncodeMediaAs + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value) + { + setting.Value = (updateSettingsDto.HostName + string.Empty).Trim(); + setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value) + { + setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl; + setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); + FlurlHttp.ConfigureClient(setting.Value, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + + _unitOfWork.SettingsRepository.Update(setting); + } + + + if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) + { + // Validate new directory can be used + if (!await _directoryService.CheckWriteAccess(bookmarkDirectory)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions")); + } + + originalBookmarkDirectory = setting.Value; + // Normalize the path deliminators. Just to look nice in DB, no functionality + setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); + _unitOfWork.SettingsRepository.Update(setting); + updateBookmarks = true; + + } + + if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.AllowStatCollection + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + if (!updateSettingsDto.AllowStatCollection) + { + _taskScheduler.CancelStatsTasks(); + } + else + { + await _taskScheduler.ScheduleStatsTasks(); + } + } + + if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value) + { + if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups")); + } + setting.Value = updateSettingsDto.TotalBackups + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TotalLogs && updateSettingsDto.TotalLogs + string.Empty != setting.Value) + { + if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs")); + } + setting.Value = updateSettingsDto.TotalLogs + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + + if (updateSettingsDto.EnableFolderWatching) + { + await _libraryWatcher.StartWatching(); + } + else + { + _libraryWatcher.StopWatching(); + } + } + } + + if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto); + try { - var d = await _settingsService.UpdateSettings(updateSettingsDto); - return Ok(d); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); + await _unitOfWork.CommitAsync(); + + if (updateBookmarks) + { + _directoryService.ExistOrCreate(bookmarkDirectory); + _directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); + _directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); + } } catch (Exception ex) { _logger.LogError(ex, "There was an exception when updating server settings"); + await _unitOfWork.RollbackAsync(); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); } + + + _logger.LogInformation("Server Settings updated"); + await _taskScheduler.ScheduleTasks(); + return Ok(updateSettingsDto); } - /// - /// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup. - /// - /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("task-frequencies")] public ActionResult> GetTaskFrequencies() @@ -187,7 +408,7 @@ public class SettingsController : BaseApiController [HttpGet("log-levels")] public ActionResult> GetLogLevels() { - return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"}); + return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"}); } [HttpGet("opds-enabled")] @@ -196,61 +417,4 @@ public class SettingsController : BaseApiController var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.EnableOpds); } - - /// - /// Is the cron expression valid for Kavita's scheduler - /// - /// - /// - [HttpGet("is-valid-cron")] - public ActionResult IsValidCron(string cronExpression) - { - // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique - return Ok(CronHelper.IsValidCron(cronExpression)); - } - - /// - /// Sends a test email to see if email settings are hooked up correctly - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("test-email-url")] - public async Task> TestEmailServiceUrl() - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); - if (string.IsNullOrEmpty(user?.Email)) return BadRequest("Your account has no email on record. Cannot email."); - return Ok(await _emailService.SendTestEmail(user!.Email)); - } - - /// - /// Get the metadata settings for Kavita+ users. - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpGet("metadata-settings")] - public async Task> GetMetadataSettings() - { - return Ok(await _unitOfWork.SettingsRepository.GetMetadataSettingDto()); - - } - - /// - /// Update the metadata settings for Kavita+ Metadata feature - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("metadata-settings")] - public async Task> UpdateMetadataSettings(MetadataSettingsDto dto) - { - try - { - return Ok(await _settingsService.UpdateMetadataSettings(dto)); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue when updating metadata settings"); - return BadRequest(ex.Message); - } - } } diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index 383905edd..625ff38ba 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.RegularExpressions; using System.Threading.Tasks; using API.Constants; using API.Data; @@ -10,39 +7,27 @@ using API.DTOs.Statistics; using API.Entities; using API.Entities.Enums; using API.Extensions; -using API.Helpers; using API.Services; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using CsvHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using MimeTypes; namespace API.Controllers; -#nullable enable - public class StatsController : BaseApiController { private readonly IStatisticService _statService; private readonly IUnitOfWork _unitOfWork; private readonly UserManager _userManager; private readonly ILocalizationService _localizationService; - private readonly ILicenseService _licenseService; - private readonly IDirectoryService _directoryService; public StatsController(IStatisticService statService, IUnitOfWork unitOfWork, - UserManager userManager, ILocalizationService localizationService, - ILicenseService licenseService, IDirectoryService directoryService) + UserManager userManager, ILocalizationService localizationService) { _statService = statService; _unitOfWork = unitOfWork; _userManager = userManager; _localizationService = localizationService; - _licenseService = licenseService; - _directoryService = directoryService; } [HttpGet("user/{userId}/read")] @@ -121,34 +106,6 @@ public class StatsController : BaseApiController return Ok(await _statService.GetFileBreakdown()); } - /// - /// Generates a csv of all file paths for a given extension - /// - /// - [Authorize("RequireAdminRole")] - [HttpGet("server/file-extension")] - [ResponseCache(CacheProfileName = "Statistics")] - public async Task DownloadFilesByExtension(string fileExtension) - { - if (!Regex.IsMatch(fileExtension, Parser.SupportedExtensions)) - { - return BadRequest("Invalid file format"); - } - var tempFile = Path.Join(_directoryService.TempDirectory, - $"file_breakdown_{fileExtension.Replace(".", string.Empty)}.csv"); - - if (!_directoryService.FileSystem.File.Exists(tempFile)) - { - var results = await _statService.GetFilesByExtension(fileExtension); - await using var writer = new StreamWriter(tempFile); - await using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); - await csv.WriteRecordsAsync(results); - } - - return PhysicalFile(tempFile, MimeTypeMap.GetMimeType(Path.GetExtension(tempFile)), - System.Web.HttpUtility.UrlEncode(Path.GetFileName(tempFile)), true); - } - /// /// Returns reading history events for a give or all users, broken up by day, and format @@ -222,4 +179,6 @@ public class StatsController : BaseApiController return Ok(_statService.GetWordsReadCountByYear(userId)); } + + } diff --git a/API/Controllers/StreamController.cs b/API/Controllers/StreamController.cs index 049885e78..11418e986 100644 --- a/API/Controllers/StreamController.cs +++ b/API/Controllers/StreamController.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.DTOs.Dashboard; using API.DTOs.SideNav; @@ -11,8 +10,6 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream) /// @@ -20,13 +17,13 @@ public class StreamController : BaseApiController { private readonly IStreamService _streamService; private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; + private readonly ILogger _logger; - public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILocalizationService localizationService) + public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILogger logger) { _streamService = streamService; _unitOfWork = unitOfWork; - _localizationService = localizationService; + _logger = logger; } /// @@ -77,7 +74,6 @@ public class StreamController : BaseApiController [HttpPost("update-external-source")] public async Task> UpdateExternalSource(ExternalSourceDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); // Check if a host and api key exists for the current user return Ok(await _streamService.UpdateExternalSource(User.GetUserId(), dto)); } @@ -90,8 +86,7 @@ public class StreamController : BaseApiController [HttpGet("external-source-exists")] public async Task> ExternalSourceExists(string host, string name, string apiKey) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - return Ok(await _unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(User.GetUserId(), name, host, apiKey)); + return Ok(await _unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(User.GetUserId(), host, name, apiKey)); } /// @@ -102,7 +97,6 @@ public class StreamController : BaseApiController [HttpDelete("delete-external-source")] public async Task ExternalSourceExists(int externalSourceId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.DeleteExternalSource(User.GetUserId(), externalSourceId); return Ok(); } @@ -116,7 +110,6 @@ public class StreamController : BaseApiController [HttpPost("add-dashboard-stream")] public async Task> AddDashboard([FromQuery] int smartFilterId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); return Ok(await _streamService.CreateDashboardStreamFromSmartFilter(User.GetUserId(), smartFilterId)); } @@ -128,7 +121,6 @@ public class StreamController : BaseApiController [HttpPost("update-dashboard-stream")] public async Task UpdateDashboardStream(DashboardStreamDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.UpdateDashboardStream(User.GetUserId(), dto); return Ok(); } @@ -141,7 +133,6 @@ public class StreamController : BaseApiController [HttpPost("update-dashboard-position")] public async Task UpdateDashboardStreamPosition(UpdateStreamPositionDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.UpdateDashboardStreamPosition(User.GetUserId(), dto); return Ok(); } @@ -155,7 +146,6 @@ public class StreamController : BaseApiController [HttpPost("add-sidenav-stream")] public async Task> AddSideNav([FromQuery] int smartFilterId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); return Ok(await _streamService.CreateSideNavStreamFromSmartFilter(User.GetUserId(), smartFilterId)); } @@ -167,7 +157,6 @@ public class StreamController : BaseApiController [HttpPost("add-sidenav-stream-from-external-source")] public async Task> AddSideNavFromExternalSource([FromQuery] int externalSourceId) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); return Ok(await _streamService.CreateSideNavStreamFromExternalSource(User.GetUserId(), externalSourceId)); } @@ -179,7 +168,6 @@ public class StreamController : BaseApiController [HttpPost("update-sidenav-stream")] public async Task UpdateSideNavStream(SideNavStreamDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.UpdateSideNavStream(User.GetUserId(), dto); return Ok(); } @@ -192,7 +180,6 @@ public class StreamController : BaseApiController [HttpPost("update-sidenav-position")] public async Task UpdateSideNavStreamPosition(UpdateStreamPositionDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.UpdateSideNavStreamPosition(User.GetUserId(), dto); return Ok(); } @@ -200,34 +187,7 @@ public class StreamController : BaseApiController [HttpPost("bulk-sidenav-stream-visibility")] public async Task BulkUpdateSideNavStream(BulkUpdateSideNavStreamVisibilityDto dto) { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); await _streamService.UpdateSideNavStreamBulk(User.GetUserId(), dto); return Ok(); } - - /// - /// Removes a Smart Filter from a user's SideNav Streams - /// - /// - /// - [HttpDelete("smart-filter-side-nav-stream")] - public async Task DeleteSmartFilterSideNavStream([FromQuery] int sideNavStreamId) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - await _streamService.DeleteSideNavSmartFilterStream(User.GetUserId(), sideNavStreamId); - return Ok(); - } - - /// - /// Removes a Smart Filter from a user's Dashboard Streams - /// - /// - /// - [HttpDelete("smart-filter-dashboard-stream")] - public async Task DeleteSmartFilterDashboardStream([FromQuery] int dashboardStreamId) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - await _streamService.DeleteDashboardSmartFilterStream(User.GetUserId(), dashboardStreamId); - return Ok(); - } } diff --git a/API/Controllers/TachiyomiController.cs b/API/Controllers/TachiyomiController.cs index e55dc3365..900783097 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/API/Controllers/TachiyomiController.cs @@ -8,8 +8,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index 9e4cee20c..2b9284f27 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -1,43 +1,30 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.DTOs.Theme; -using API.Entities; using API.Extensions; using API.Services; using API.Services.Tasks; -using AutoMapper; using Kavita.Common; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; namespace API.Controllers; -#nullable enable - public class ThemeController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IThemeService _themeService; + private readonly ITaskScheduler _taskScheduler; private readonly ILocalizationService _localizationService; - private readonly IDirectoryService _directoryService; - private readonly IMapper _mapper; - - public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, - ILocalizationService localizationService, IDirectoryService directoryService, IMapper mapper) + public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler, + ILocalizationService localizationService) { _unitOfWork = unitOfWork; _themeService = themeService; + _taskScheduler = taskScheduler; _localizationService = localizationService; - _directoryService = directoryService; - _mapper = mapper; } [ResponseCache(CacheProfileName = "10Minute")] @@ -48,6 +35,13 @@ public class ThemeController : BaseApiController return Ok(await _unitOfWork.SiteThemeRepository.GetThemeDtos()); } + [Authorize("RequireAdminRole")] + [HttpPost("scan")] + public ActionResult Scan() + { + _taskScheduler.ScanSiteThemes(); + return Ok(); + } [Authorize("RequireAdminRole")] [HttpPost("update-default")] @@ -57,7 +51,7 @@ public class ThemeController : BaseApiController { await _themeService.UpdateDefault(dto.ThemeId); } - catch (KavitaException) + catch (KavitaException ex) { return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist")); } @@ -82,70 +76,4 @@ public class ThemeController : BaseApiController return BadRequest(await _localizationService.Get("en", ex.Message)); } } - - /// - /// Browse themes that can be used on this server - /// - /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)] - [HttpGet("browse")] - public async Task>> BrowseThemes() - { - var themes = await _themeService.GetDownloadableThemes(); - return Ok(themes.Where(t => !t.AlreadyDownloaded)); - } - - /// - /// Attempts to delete a theme. If already in use by users, will not allow - /// - /// - /// - [HttpDelete] - public async Task>> DeleteTheme(int themeId) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - await _themeService.DeleteTheme(themeId); - - return Ok(); - } - - /// - /// Downloads a SiteTheme from upstream - /// - /// - /// - [HttpPost("download-theme")] - public async Task> DownloadTheme(DownloadableSiteThemeDto dto) - { - return Ok(_mapper.Map(await _themeService.DownloadRepoTheme(dto))); - } - - /// - /// Uploads a new theme file - /// - /// - /// - [HttpPost("upload-theme")] - public async Task> DownloadTheme(IFormFile formFile) - { - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - - if (!formFile.FileName.EndsWith(".css")) return BadRequest("Invalid file"); - if (formFile.FileName.Contains("..")) return BadRequest("Invalid file"); - var tempFile = await UploadToTemp(formFile); - - // Set summary as "Uploaded by User.GetUsername() on DATE" - var theme = await _themeService.CreateThemeFromFile(tempFile, User.GetUsername()); - return Ok(_mapper.Map(theme)); - } - - private async Task UploadToTemp(IFormFile file) - { - var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); - await using var stream = System.IO.File.Create(outputFile); - await file.CopyToAsync(stream); - stream.Close(); - return outputFile; - } - } diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 9652ba494..ab01d7abb 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -1,15 +1,10 @@ using System; -using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; -using API.Data.Repositories; using API.DTOs.Uploads; -using API.Entities.Enums; -using API.Entities.MetadataMatching; using API.Extensions; using API.Services; -using API.Services.Tasks.Metadata; using API.SignalR; using Flurl.Http; using Microsoft.AspNetCore.Authorization; @@ -18,8 +13,6 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; -#nullable enable - /// /// /// @@ -33,12 +26,11 @@ public class UploadController : BaseApiController private readonly IEventHub _eventHub; private readonly IReadingListService _readingListService; private readonly ILocalizationService _localizationService; - private readonly ICoverDbService _coverDbService; /// public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger logger, ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService, - ILocalizationService localizationService, ICoverDbService coverDbService) + ILocalizationService localizationService) { _unitOfWork = unitOfWork; _imageService = imageService; @@ -48,7 +40,6 @@ public class UploadController : BaseApiController _eventHub = eventHub; _readingListService = readingListService; _localizationService = localizationService; - _coverDbService = coverDbService; } /// @@ -97,35 +88,26 @@ public class UploadController : BaseApiController { // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. // See if we can do this all in memory without touching underlying system + if (string.IsNullOrEmpty(uploadFileDto.Url)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required")); + } + try { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id); - if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist")); + var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}"); - var filePath = string.Empty; - var lockState = false; - if (!string.IsNullOrEmpty(uploadFileDto.Url)) + if (!string.IsNullOrEmpty(filePath)) { - filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}"); - lockState = uploadFileDto.LockCover; + series.CoverImage = filePath; + series.CoverImageLocked = true; + _unitOfWork.SeriesRepository.Update(series); } - series.CoverImage = filePath; - series.CoverImageLocked = lockState; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); - _imageService.UpdateColorScape(series); - _unitOfWork.SeriesRepository.Update(series); - _unitOfWork.SeriesRepository.Update(series.Metadata); - if (_unitOfWork.HasChanges()) { - // Refresh covers - if (string.IsNullOrEmpty(uploadFileDto.Url)) - { - _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); - } - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); await _unitOfWork.CommitAsync(); @@ -154,24 +136,24 @@ public class UploadController : BaseApiController { // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. // See if we can do this all in memory without touching underlying system + if (string.IsNullOrEmpty(uploadFileDto.Url)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required")); + } + try { - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(uploadFileDto.Id); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id); if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist")); + var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}"); - var filePath = string.Empty; - var lockState = false; - if (!string.IsNullOrEmpty(uploadFileDto.Url)) + if (!string.IsNullOrEmpty(filePath)) { - filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}"); - lockState = uploadFileDto.LockCover; + tag.CoverImage = filePath; + tag.CoverImageLocked = true; + _unitOfWork.CollectionTagRepository.Update(tag); } - tag.CoverImage = filePath; - tag.CoverImageLocked = lockState; - _imageService.UpdateColorScape(tag); - _unitOfWork.CollectionTagRepository.Update(tag); - if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); @@ -200,31 +182,29 @@ public class UploadController : BaseApiController [HttpPost("reading-list")] public async Task UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto) { - // Check if Url is non-empty, request the image and place in temp, then ask image service to handle it. + // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. // See if we can do this all in memory without touching underlying system - if (await _readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null) + if (string.IsNullOrEmpty(uploadFileDto.Url)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required")); + } + + if (_readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied")); try { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id); if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist")); + var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}"); - - var filePath = string.Empty; - var lockState = false; - if (!string.IsNullOrEmpty(uploadFileDto.Url)) + if (!string.IsNullOrEmpty(filePath)) { - filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}"); - lockState = uploadFileDto.LockCover; + readingList.CoverImage = filePath; + readingList.CoverImageLocked = true; + _unitOfWork.ReadingListRepository.Update(readingList); } - - readingList.CoverImage = filePath; - readingList.CoverImageLocked = lockState; - _imageService.UpdateColorScape(readingList); - _unitOfWork.ReadingListRepository.Update(readingList); - if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); @@ -243,14 +223,17 @@ public class UploadController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save")); } - private async Task CreateThumbnail(UploadFileDto uploadFileDto, string filename) + private async Task CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var encodeFormat = settings.EncodeMediaAs; - var coverImageSize = settings.CoverImageSize; + var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + if (thumbnailSize > 0) + { + return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, + filename, encodeFormat, thumbnailSize); + } return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, - filename, encodeFormat, coverImageSize.GetDimensions().Width); + filename, encodeFormat); } /// @@ -265,43 +248,33 @@ public class UploadController : BaseApiController { // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. // See if we can do this all in memory without touching underlying system + if (string.IsNullOrEmpty(uploadFileDto.Url)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required")); + } + try { var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}"); - var filePath = string.Empty; - var lockState = false; - if (!string.IsNullOrEmpty(uploadFileDto.Url)) + if (!string.IsNullOrEmpty(filePath)) { - filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}"); - lockState = uploadFileDto.LockCover; - } - - chapter.CoverImage = filePath; - chapter.CoverImageLocked = lockState; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterCovers); - _unitOfWork.ChapterRepository.Update(chapter); - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); - if (volume != null) - { - volume.CoverImage = chapter.CoverImage; - volume.CoverImageLocked = lockState; - _unitOfWork.VolumeRepository.Update(volume); + chapter.CoverImage = filePath; + chapter.CoverImageLocked = true; + _unitOfWork.ChapterRepository.Update(chapter); + var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); + if (volume != null) + { + volume.CoverImage = chapter.CoverImage; + _unitOfWork.VolumeRepository.Update(volume); + } } if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); - - // Refresh covers - if (string.IsNullOrEmpty(uploadFileDto.Url)) - { - var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId))!; - _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); - } - - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, @@ -319,67 +292,6 @@ public class UploadController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-chapter-save")); } - /// - /// Replaces volume cover image and locks it with a base64 encoded image. - /// - /// This will not update the underlying chapter - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] - [HttpPost("volume")] - public async Task UploadVolumeCoverImageFromUrl(UploadFileDto uploadFileDto) - { - // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. - // See if we can do this all in memory without touching underlying system - try - { - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(uploadFileDto.Id, VolumeIncludes.Chapters); - if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); - - var filePath = string.Empty; - var lockState = false; - if (!string.IsNullOrEmpty(uploadFileDto.Url)) - { - filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetVolumeFormat(uploadFileDto.Id)}"); - lockState = uploadFileDto.LockCover; - } - - volume.CoverImage = filePath; - volume.CoverImageLocked = lockState; - _imageService.UpdateColorScape(volume); - _unitOfWork.VolumeRepository.Update(volume); - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - - // Refresh covers - if (string.IsNullOrEmpty(uploadFileDto.Url)) - { - var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; - _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); - } - - - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(uploadFileDto.Id, MessageFactoryEntityTypes.Volume), false); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Chapter), false); - return Ok(); - } - - } - catch (Exception e) - { - _logger.LogError(e, "There was an issue uploading cover image for Volume {Id}", uploadFileDto.Id); - await _unitOfWork.RollbackAsync(); - } - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-volume-save")); - } - - /// /// Replaces library cover image with a base64 encoded image. If empty string passed, will reset to null. /// @@ -398,7 +310,6 @@ public class UploadController : BaseApiController if (string.IsNullOrEmpty(uploadFileDto.Url)) { library.CoverImage = null; - library.ResetColorScape(); _unitOfWork.LibraryRepository.Update(library); if (_unitOfWork.HasChanges()) { @@ -413,12 +324,12 @@ public class UploadController : BaseApiController try { var filePath = await CreateThumbnail(uploadFileDto, - $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}"); + $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", + ImageService.LibraryThumbnailWidth); if (!string.IsNullOrEmpty(filePath)) { library.CoverImage = filePath; - _imageService.UpdateColorScape(library); _unitOfWork.LibraryRepository.Update(library); } @@ -447,7 +358,6 @@ public class UploadController : BaseApiController /// [Authorize(Policy = "RequireAdminRole")] [HttpPost("reset-chapter-lock")] - [Obsolete("Use LockCover in UploadFileDto")] public async Task ResetChapterLock(UploadFileDto uploadFileDto) { try @@ -455,15 +365,12 @@ public class UploadController : BaseApiController var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); var originalFile = chapter.CoverImage; - chapter.CoverImage = string.Empty; chapter.CoverImageLocked = false; _unitOfWork.ChapterRepository.Update(chapter); - var volume = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!; volume.CoverImage = chapter.CoverImage; _unitOfWork.VolumeRepository.Update(volume); - var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; if (_unitOfWork.HasChanges()) @@ -484,32 +391,4 @@ public class UploadController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock")); } - /// - /// Replaces person tag cover image and locks it with a base64 encoded image - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] - [HttpPost("person")] - public async Task UploadPersonCoverImageFromUrl(UploadFileDto uploadFileDto) - { - try - { - var person = await _unitOfWork.PersonRepository.GetPersonById(uploadFileDto.Id); - if (person == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); - - await _coverDbService.SetPersonCoverByUrl(person, uploadFileDto.Url, true); - return Ok(); - } - catch (Exception e) - { - _logger.LogError(e, "There was an issue uploading cover image for Person {Id}", uploadFileDto.Id); - await _unitOfWork.RollbackAsync(); - } - - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-person-save")); - } - - } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 17ebc758e..9358bd406 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,14 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.KavitaPlus.Account; using API.Extensions; using API.Services; -using API.Services.Plus; using API.SignalR; using AutoMapper; using Microsoft.AspNetCore.Authorization; @@ -16,8 +13,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - [Authorize] public class UsersController : BaseApiController { @@ -25,16 +20,14 @@ public class UsersController : BaseApiController private readonly IMapper _mapper; private readonly IEventHub _eventHub; private readonly ILocalizationService _localizationService; - private readonly ILicenseService _licenseService; public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub, - ILocalizationService localizationService, ILicenseService licenseService) + ILocalizationService localizationService) { _unitOfWork = unitOfWork; _mapper = mapper; _eventHub = eventHub; _localizationService = localizationService; - _licenseService = licenseService; } [Authorize(Policy = "RequireAdminRole")] @@ -87,50 +80,47 @@ public class UsersController : BaseApiController return Ok(libs.Any(x => x.Id == libraryId)); } - /// - /// Update the user preferences - /// - /// If the user has ReadOnly role, they will not be able to perform this action - /// - /// [HttpPost("update-preferences")] public async Task> UpdatePreferences(UserPreferencesDto preferencesDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.UserPreferences); if (user == null) return Unauthorized(); - if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var existingPreferences = user!.UserPreferences; + existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; + existingPreferences.ScalingOption = preferencesDto.ScalingOption; + existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; + existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu; + existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints; + existingPreferences.EmulateBook = preferencesDto.EmulateBook; + existingPreferences.ReaderMode = preferencesDto.ReaderMode; + existingPreferences.LayoutMode = preferencesDto.LayoutMode; + existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor; + existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin; + existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing; + existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily; + existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; + existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate; + existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; + existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle; + existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName; + existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode; + existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode; existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries; + existingPreferences.LayoutMode = preferencesDto.LayoutMode; + existingPreferences.Theme = preferencesDto.Theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; + existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; existingPreferences.ShareReviews = preferencesDto.ShareReviews; - - if (await _licenseService.HasActiveLicense()) - { - existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; - existingPreferences.WantToReadSync = preferencesDto.WantToReadSync; - } - - - - if (preferencesDto.Theme != null && existingPreferences.Theme.Id != preferencesDto.Theme?.Id) - { - var theme = await _unitOfWork.SiteThemeRepository.GetTheme(preferencesDto.Theme!.Id); - existingPreferences.Theme = theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); - } - - - if (_localizationService.GetLocales().Select(l => l.FileName).Contains(preferencesDto.Locale)) + if (_localizationService.GetLocales().Contains(preferencesDto.Locale)) { existingPreferences.Locale = preferencesDto.Locale; } - _unitOfWork.UserRepository.Update(existingPreferences); if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-pref")); @@ -161,18 +151,4 @@ public class UsersController : BaseApiController { return Ok((await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName)); } - - /// - /// Returns all users with tokens registered and their token information. Does not send the tokens. - /// - /// Kavita+ only - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpGet("tokens")] - public async Task>> GetUserTokens() - { - if (!await _licenseService.HasActiveLicense()) return BadRequest(_localizationService.Translate(User.GetUserId(), "kavitaplus-restricted")); - - return Ok((await _unitOfWork.UserRepository.GetUserTokenInfo())); - } } diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs deleted file mode 100644 index db1381d9d..000000000 --- a/API/Controllers/VolumeController.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Extensions; -using API.Services; -using API.SignalR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; -#nullable enable - -public class VolumeController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - - public VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - } - - /// - /// Returns the appropriate Volume - /// - /// - /// - [HttpGet] - public async Task> GetVolume(int volumeId) - { - return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId())); - } - - [Authorize(Policy = "RequireAdminRole")] - [HttpDelete] - public async Task> DeleteVolume(int volumeId) - { - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, - VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags); - if (volume == null) - return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); - - _unitOfWork.VolumeRepository.Remove(volume); - - if (await _unitOfWork.CommitAsync()) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - return Ok(true); - } - - return Ok(false); - } - - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("multiple")] - public async Task> DeleteMultipleVolumes(int[] volumesIds) - { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds); - if (volumes.Count != volumesIds.Length) - { - return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); - } - - _unitOfWork.VolumeRepository.Remove(volumes); - - if (!await _unitOfWork.CommitAsync()) - { - return Ok(false); - } - - foreach (var volume in volumes) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - } - - return Ok(true); - } -} diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 071a027f7..3fb33a822 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -7,7 +7,6 @@ using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.WantToRead; -using API.Entities; using API.Extensions; using API.Helpers; using API.Services; @@ -17,8 +16,6 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; -#nullable enable - /// /// Responsible for all things Want To Read /// @@ -40,13 +37,12 @@ public class WantToReadController : BaseApiController /// /// Return all Series that are in the current logged in user's Want to Read list, filtered (deprecated, use v2) /// - /// This will be removed in v0.8.x /// /// /// [HttpPost] [Obsolete("use v2 instead")] - public async Task>> GetWantToRead([FromQuery] UserParams? userParams, FilterDto filterDto) + public async Task>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto) { userParams ??= new UserParams(); var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto); @@ -64,7 +60,7 @@ public class WantToReadController : BaseApiController /// /// [HttpPost("v2")] - public async Task>> GetWantToReadV2([FromQuery] UserParams? userParams, FilterV2Dto filterDto) + public async Task>> GetWantToReadV2([FromQuery] UserParams userParams, FilterV2Dto filterDto) { userParams ??= new UserParams(); var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(User.GetUserId(), userParams, filterDto); @@ -93,15 +89,15 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - var existingIds = user.WantToRead.Select(s => s.SeriesId).ToList(); - var idsToAdd = dto.SeriesIds.Except(existingIds); + var existingIds = user.WantToRead.Select(s => s.Id).ToList(); + existingIds.AddRange(dto.SeriesIds); - foreach (var id in idsToAdd) + var idsToAdd = existingIds.Distinct().ToList(); + + var seriesToAdd = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(idsToAdd); + foreach (var series in seriesToAdd) { - user.WantToRead.Add(new AppUserWantToRead() - { - SeriesId = id - }); + user.WantToRead.Add(series); } if (!_unitOfWork.HasChanges()) return Ok(); @@ -129,9 +125,7 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - user.WantToRead = user.WantToRead - .Where(s => !dto.SeriesIds.Contains(s.SeriesId)) - .ToList(); + user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList(); if (!_unitOfWork.HasChanges()) return Ok(); if (await _unitOfWork.CommitAsync()) diff --git a/API/DTOs/Account/AgeRestrictionDto.cs b/API/DTOs/Account/AgeRestrictionDto.cs index 6505bdbff..0aaec9b97 100644 --- a/API/DTOs/Account/AgeRestrictionDto.cs +++ b/API/DTOs/Account/AgeRestrictionDto.cs @@ -2,15 +2,15 @@ namespace API.DTOs.Account; -public sealed record AgeRestrictionDto +public class AgeRestrictionDto { /// /// The maximum age rating a user has access to. -1 if not applicable /// - public required AgeRating AgeRating { get; init; } = AgeRating.NotApplicable; + public required AgeRating AgeRating { get; set; } = AgeRating.NotApplicable; /// /// Are Unknowns explicitly allowed against age rating /// /// Unknown is always lowest and default age rating. Setting this to false will ensure Teen age rating applies and unknowns are still filtered - public required bool IncludeUnknowns { get; init; } = false; + public required bool IncludeUnknowns { get; set; } = false; } diff --git a/API/DTOs/Account/AniListUpdateDto.cs b/API/DTOs/Account/AniListUpdateDto.cs new file mode 100644 index 000000000..d51a1dc0d --- /dev/null +++ b/API/DTOs/Account/AniListUpdateDto.cs @@ -0,0 +1,6 @@ +namespace API.DTOs.Account; + +public class AniListUpdateDto +{ + public string Token { get; set; } +} diff --git a/API/DTOs/Account/ConfirmEmailDto.cs b/API/DTOs/Account/ConfirmEmailDto.cs index 413f9f34a..fb9a7c470 100644 --- a/API/DTOs/Account/ConfirmEmailDto.cs +++ b/API/DTOs/Account/ConfirmEmailDto.cs @@ -2,14 +2,14 @@ namespace API.DTOs.Account; -public sealed record ConfirmEmailDto +public class ConfirmEmailDto { [Required] public string Email { get; set; } = default!; [Required] public string Token { get; set; } = default!; [Required] - [StringLength(256, MinimumLength = 6)] + [StringLength(32, MinimumLength = 6)] public string Password { get; set; } = default!; [Required] public string Username { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmEmailUpdateDto.cs b/API/DTOs/Account/ConfirmEmailUpdateDto.cs index 2a0738e35..42abb1295 100644 --- a/API/DTOs/Account/ConfirmEmailUpdateDto.cs +++ b/API/DTOs/Account/ConfirmEmailUpdateDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public sealed record ConfirmEmailUpdateDto +public class ConfirmEmailUpdateDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmMigrationEmailDto.cs b/API/DTOs/Account/ConfirmMigrationEmailDto.cs index cdfc1505c..efb42b8fd 100644 --- a/API/DTOs/Account/ConfirmMigrationEmailDto.cs +++ b/API/DTOs/Account/ConfirmMigrationEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public sealed record ConfirmMigrationEmailDto +public class ConfirmMigrationEmailDto { public string Email { get; set; } = default!; public string Token { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmPasswordResetDto.cs b/API/DTOs/Account/ConfirmPasswordResetDto.cs index 00aff301b..862a18986 100644 --- a/API/DTOs/Account/ConfirmPasswordResetDto.cs +++ b/API/DTOs/Account/ConfirmPasswordResetDto.cs @@ -2,13 +2,13 @@ namespace API.DTOs.Account; -public sealed record ConfirmPasswordResetDto +public class ConfirmPasswordResetDto { [Required] public string Email { get; set; } = default!; [Required] public string Token { get; set; } = default!; [Required] - [StringLength(256, MinimumLength = 6)] + [StringLength(32, MinimumLength = 6)] public string Password { get; set; } = default!; } diff --git a/API/DTOs/Account/InviteUserDto.cs b/API/DTOs/Account/InviteUserDto.cs index c12bebc2b..112013053 100644 --- a/API/DTOs/Account/InviteUserDto.cs +++ b/API/DTOs/Account/InviteUserDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; -public sealed record InviteUserDto +public class InviteUserDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/InviteUserResponse.cs b/API/DTOs/Account/InviteUserResponse.cs index ed16bd05e..97d7f408c 100644 --- a/API/DTOs/Account/InviteUserResponse.cs +++ b/API/DTOs/Account/InviteUserResponse.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public sealed record InviteUserResponse +public class InviteUserResponse { /// /// Email link used to setup the user account @@ -10,8 +10,4 @@ public sealed record InviteUserResponse /// Was an email sent (ie is this server accessible) /// public bool EmailSent { get; set; } = default!; - /// - /// When a user has an invalid email and is attempting to perform a flow. - /// - public bool InvalidEmail { get; set; } = false; } diff --git a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs b/API/DTOs/Account/LicenseValidDto.cs similarity index 57% rename from API/DTOs/KavitaPlus/License/LicenseValidDto.cs rename to API/DTOs/Account/LicenseValidDto.cs index a7bd476ce..f49420779 100644 --- a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs +++ b/API/DTOs/Account/LicenseValidDto.cs @@ -1,6 +1,6 @@ -namespace API.DTOs.KavitaPlus.License; +namespace API.DTOs.Account; -public sealed record LicenseValidDto +public class LicenseValidDto { public required string License { get; set; } public required string InstallId { get; set; } diff --git a/API/DTOs/Account/LoginDto.cs b/API/DTOs/Account/LoginDto.cs index 97338640b..a5376e1fc 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/API/DTOs/Account/LoginDto.cs @@ -1,7 +1,6 @@ namespace API.DTOs.Account; -#nullable enable -public sealed record LoginDto +public class LoginDto { public string Username { get; init; } = default!; public string Password { get; set; } = default!; diff --git a/API/DTOs/Account/MigrateUserEmailDto.cs b/API/DTOs/Account/MigrateUserEmailDto.cs index 4630c510f..60d042165 100644 --- a/API/DTOs/Account/MigrateUserEmailDto.cs +++ b/API/DTOs/Account/MigrateUserEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public sealed record MigrateUserEmailDto +public class MigrateUserEmailDto { public string Email { get; set; } = default!; public string Username { get; set; } = default!; diff --git a/API/DTOs/Account/ResetPasswordDto.cs b/API/DTOs/Account/ResetPasswordDto.cs index 545ca5ba6..fc7147f62 100644 --- a/API/DTOs/Account/ResetPasswordDto.cs +++ b/API/DTOs/Account/ResetPasswordDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public sealed record ResetPasswordDto +public class ResetPasswordDto { /// /// The Username of the User @@ -13,7 +13,7 @@ public sealed record ResetPasswordDto /// The new password /// [Required] - [StringLength(256, MinimumLength = 6)] + [StringLength(32, MinimumLength = 6)] public string Password { get; init; } = default!; /// /// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is. diff --git a/API/DTOs/Account/TokenRequestDto.cs b/API/DTOs/Account/TokenRequestDto.cs index 5c798721c..85ab9f87a 100644 --- a/API/DTOs/Account/TokenRequestDto.cs +++ b/API/DTOs/Account/TokenRequestDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public sealed record TokenRequestDto +public class TokenRequestDto { public string Token { get; init; } = default!; public string RefreshToken { get; init; } = default!; diff --git a/API/DTOs/Account/UpdateAgeRestrictionDto.cs b/API/DTOs/Account/UpdateAgeRestrictionDto.cs index 2fa9c89d2..ef6be1bba 100644 --- a/API/DTOs/Account/UpdateAgeRestrictionDto.cs +++ b/API/DTOs/Account/UpdateAgeRestrictionDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums; namespace API.DTOs.Account; -public sealed record UpdateAgeRestrictionDto +public class UpdateAgeRestrictionDto { [Required] public AgeRating AgeRating { get; set; } diff --git a/API/DTOs/Account/UpdateEmailDto.cs b/API/DTOs/Account/UpdateEmailDto.cs index 873862ba1..eac06be53 100644 --- a/API/DTOs/Account/UpdateEmailDto.cs +++ b/API/DTOs/Account/UpdateEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public sealed record UpdateEmailDto +public class UpdateEmailDto { public string Email { get; set; } = default!; public string Password { get; set; } = default!; diff --git a/API/DTOs/Account/UpdateEmailResponse.cs b/API/DTOs/Account/UpdateEmailResponse.cs new file mode 100644 index 000000000..4f9b816c1 --- /dev/null +++ b/API/DTOs/Account/UpdateEmailResponse.cs @@ -0,0 +1,14 @@ +namespace API.DTOs.Account; + +public class UpdateEmailResponse +{ + /// + /// Did the user not have an existing email + /// + /// This informs the user to check the new email address + public bool HadNoExistingEmail { get; set; } + /// + /// Was an email sent (ie is this server accessible) + /// + public bool EmailSent { get; set; } +} diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index 0cb0eaf66..bda664bdb 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -2,18 +2,13 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; -#nullable enable -public sealed record UpdateUserDto +public record UpdateUserDto { - /// public int UserId { get; set; } - /// public string Username { get; set; } = default!; - /// /// List of Roles to assign to user. If admin not present, Pleb will be applied. /// If admin present, all libraries will be granted access and will ignore those from DTO. - /// public IList Roles { get; init; } = default!; /// /// A list of libraries to grant access to @@ -23,6 +18,4 @@ public sealed record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; - /// - public string? Email { get; set; } = default!; } diff --git a/API/DTOs/BulkActionDto.cs b/API/DTOs/BulkActionDto.cs deleted file mode 100644 index c26a73e9c..000000000 --- a/API/DTOs/BulkActionDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs; - -public sealed record BulkActionDto -{ - public List Ids { get; set; } - /** - * If this is a Scan action, will ignore optimizations - */ - public bool? Force { get; set; } -} diff --git a/API/DTOs/ChapterDetailPlusDto.cs b/API/DTOs/ChapterDetailPlusDto.cs deleted file mode 100644 index d99482e55..000000000 --- a/API/DTOs/ChapterDetailPlusDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using API.DTOs.SeriesDetail; - -namespace API.DTOs; - -public sealed record ChapterDetailPlusDto -{ - public float Rating { get; set; } - public bool HasBeenRated { get; set; } - - public IList Reviews { get; set; } = []; - public IList Ratings { get; set; } = []; -} diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 85624b51c..26a8b8459 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -1,37 +1,36 @@ using System; using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Interfaces; namespace API.DTOs; -#nullable enable /// /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying /// file (abstracted from type). /// -public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage +public class ChapterDto : IHasReadTimeEstimate { - /// public int Id { get; init; } - /// + /// + /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". + /// public string Range { get; init; } = default!; - /// - [Obsolete("Use MinNumber and MaxNumber instead")] + /// + /// Smallest number of the Range. + /// public string Number { get; init; } = default!; - /// - public float MinNumber { get; init; } - /// - public float MaxNumber { get; init; } - /// - public float SortOrder { get; set; } - /// + /// + /// Total number of pages in all MangaFiles + /// public int Pages { get; init; } - /// + /// + /// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename + /// public bool IsSpecial { get; init; } - /// + /// + /// Used for books/specials to display custom title. For non-specials/books, will be set to + /// public string Title { get; set; } = default!; /// /// The files that represent this Chapter @@ -49,25 +48,46 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// The last time a chapter was read by current authenticated user /// public DateTime LastReadingProgress { get; set; } - /// + /// + /// If the Cover Image is locked for this entity + /// public bool CoverImageLocked { get; set; } - /// + /// + /// Volume Id this Chapter belongs to + /// public int VolumeId { get; init; } - /// + /// + /// When chapter was created + /// public DateTime CreatedUtc { get; set; } - /// public DateTime LastModifiedUtc { get; set; } - /// + /// + /// When chapter was created in local server time + /// + /// This is required for Tachiyomi Extension public DateTime Created { get; set; } - /// + /// + /// When the chapter was released. + /// + /// Metadata field public DateTime ReleaseDate { get; init; } - /// + /// + /// Title of the Chapter/Issue + /// + /// Metadata field public string TitleName { get; set; } = default!; - /// + /// + /// Summary of the Chapter + /// + /// This is not set normally, only for Series Detail public string Summary { get; init; } = default!; - /// + /// + /// Age Rating for the issue/chapter + /// public AgeRating AgeRating { get; init; } - /// + /// + /// Total words in a Chapter (books only) + /// public long WordCount { get; set; } = 0L; /// /// Formatted Volume title ie) Volume 2. @@ -79,93 +99,14 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// public int MaxHoursToRead { get; set; } /// - public float AvgHoursToRead { get; set; } - /// - public string WebLinks { get; set; } - /// - public string ISBN { get; set; } - - #region Metadata - - public ICollection Writers { get; set; } = new List(); - public ICollection CoverArtists { get; set; } = new List(); - public ICollection Publishers { get; set; } = new List(); - public ICollection Characters { get; set; } = new List(); - public ICollection Pencillers { get; set; } = new List(); - public ICollection Inkers { get; set; } = new List(); - public ICollection Imprints { get; set; } = new List(); - public ICollection Colorists { get; set; } = new List(); - public ICollection Letterers { get; set; } = new List(); - public ICollection Editors { get; set; } = new List(); - public ICollection Translators { get; set; } = new List(); - public ICollection Teams { get; set; } = new List(); - public ICollection Locations { get; set; } = new List(); - - public ICollection Genres { get; set; } = new List(); - + public int AvgHoursToRead { get; set; } /// - /// Collection of all Tags from underlying chapters for a Series + /// Comma-separated link of urls to external services that have some relation to the Chapter /// - public ICollection Tags { get; set; } = new List(); - public PublicationStatus PublicationStatus { get; set; } - /// - public string? Language { get; set; } - /// - public int Count { get; set; } - /// - public int TotalCount { get; set; } - - /// - public bool LanguageLocked { get; set; } - /// - public bool SummaryLocked { get; set; } - /// - public bool AgeRatingLocked { get; set; } - public bool PublicationStatusLocked { get; set; } - /// - public bool GenresLocked { get; set; } - /// - public bool TagsLocked { get; set; } - /// - public bool WriterLocked { get; set; } - /// - public bool CharacterLocked { get; set; } - /// - public bool ColoristLocked { get; set; } - /// - public bool EditorLocked { get; set; } - /// - public bool InkerLocked { get; set; } - /// - public bool ImprintLocked { get; set; } - /// - public bool LettererLocked { get; set; } - /// - public bool PencillerLocked { get; set; } - /// - public bool PublisherLocked { get; set; } - /// - public bool TranslatorLocked { get; set; } - /// - public bool TeamLocked { get; set; } - /// - public bool LocationLocked { get; set; } - /// - public bool CoverArtistLocked { get; set; } - public bool ReleaseYearLocked { get; set; } - - #endregion - - /// - public string? CoverImage { get; set; } - /// - public string? PrimaryColor { get; set; } = string.Empty; - /// - public string? SecondaryColor { get; set; } = string.Empty; - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } + public string WebLinks { get; set; } + /// + /// ISBN-13 (usually) of the Chapter + /// + /// This is guaranteed to be Valid + public string ISBN { get; set; } } diff --git a/API/DTOs/Collection/AppUserCollectionDto.cs b/API/DTOs/Collection/AppUserCollectionDto.cs deleted file mode 100644 index 0634b5d83..000000000 --- a/API/DTOs/Collection/AppUserCollectionDto.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; - -namespace API.DTOs.Collection; -#nullable enable - -public sealed record AppUserCollectionDto : IHasCoverImage -{ - public int Id { get; init; } - public string Title { get; init; } = default!; - public string? Summary { get; init; } = default!; - public bool Promoted { get; init; } - public AgeRating AgeRating { get; init; } - - /// - /// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set. - /// - public string? CoverImage { get; set; } = string.Empty; - - public string? PrimaryColor { get; set; } = string.Empty; - public string? SecondaryColor { get; set; } = string.Empty; - public bool CoverImageLocked { get; init; } - - /// - /// Number of Series in the Collection - /// - public int ItemCount { get; init; } - - /// - /// Owner of the Collection - /// - public string? Owner { get; init; } - /// - /// Last time Kavita Synced the Collection with an upstream source (for non Kavita sourced collections) - /// - public DateTime LastSyncUtc { get; init; } - /// - /// Who created/manages the list. Non-Kavita lists are not editable by the user, except to promote - /// - public ScrobbleProvider Source { get; init; } = ScrobbleProvider.Kavita; - /// - /// For Non-Kavita sourced collections, the url to sync from - /// - public string? SourceUrl { get; init; } - /// - /// Total number of items as of the last sync. Not applicable for Kavita managed collections. - /// - public int TotalSourceCount { get; init; } - /// - /// A
separated string of all missing series - ///
- public string? MissingSeriesFromSource { get; init; } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } -} diff --git a/API/DTOs/Collection/DeleteCollectionsDto.cs b/API/DTOs/Collection/DeleteCollectionsDto.cs deleted file mode 100644 index c0b94e9a1..000000000 --- a/API/DTOs/Collection/DeleteCollectionsDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs.Collection; - -public class DeleteCollectionsDto -{ - [Required] - public IList CollectionIds { get; set; } -} diff --git a/API/DTOs/Collection/MalStackDto.cs b/API/DTOs/Collection/MalStackDto.cs deleted file mode 100644 index d9d902e88..000000000 --- a/API/DTOs/Collection/MalStackDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace API.DTOs.Collection; -#nullable enable - -/// -/// Represents an Interest Stack from MAL -/// -public class MalStackDto -{ - public required string Title { get; set; } - public required long StackId { get; set; } - public required string Url { get; set; } - public required string? Author { get; set; } - public required int SeriesCount { get; set; } - public required int RestackCount { get; set; } - /// - /// If an existing collection exists within Kavita - /// - /// This is filled out from Kavita and not Kavita+ - public int ExistingId { get; set; } -} diff --git a/API/DTOs/Collection/PromoteCollectionsDto.cs b/API/DTOs/Collection/PromoteCollectionsDto.cs deleted file mode 100644 index 2e2ab793b..000000000 --- a/API/DTOs/Collection/PromoteCollectionsDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs.Collection; - -public class PromoteCollectionsDto -{ - public IList CollectionIds { get; init; } - public bool Promoted { get; init; } -} diff --git a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs index 0a2270fbf..1d078959d 100644 --- a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.CollectionTags; -public sealed record CollectionTagBulkAddDto +public class CollectionTagBulkAddDto { /// /// Collection Tag Id diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/API/DTOs/CollectionTags/CollectionTagDto.cs index 911622051..2a1279a35 100644 --- a/API/DTOs/CollectionTags/CollectionTagDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagDto.cs @@ -1,23 +1,14 @@ -using System; +namespace API.DTOs.CollectionTags; -namespace API.DTOs.CollectionTags; - -[Obsolete("Use AppUserCollectionDto")] -public sealed record CollectionTagDto +public class CollectionTagDto { - /// public int Id { get; set; } - /// public string Title { get; set; } = default!; - /// public string Summary { get; set; } = default!; - /// public bool Promoted { get; set; } /// /// The cover image string. This is used on Frontend to show or hide the Cover Image /// - /// public string CoverImage { get; set; } = default!; - /// public bool CoverImageLocked { get; set; } } diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs index 139834a60..9d6f2a035 100644 --- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs +++ b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Collection; +using System.Collections.Generic; namespace API.DTOs.CollectionTags; -public sealed record UpdateSeriesForTagDto +public class UpdateSeriesForTagDto { - public AppUserCollectionDto Tag { get; init; } = default!; + public CollectionTagDto Tag { get; init; } = default!; public IEnumerable SeriesIdsToRemove { get; init; } = default!; } diff --git a/API/DTOs/ColorScape.cs b/API/DTOs/ColorScape.cs deleted file mode 100644 index 5351f2351..000000000 --- a/API/DTOs/ColorScape.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace API.DTOs; -#nullable enable - -/// -/// A primary and secondary color -/// -public sealed record ColorScape -{ - public required string? Primary { get; set; } - public required string? Secondary { get; set; } -} diff --git a/API/DTOs/CopySettingsFromLibraryDto.cs b/API/DTOs/CopySettingsFromLibraryDto.cs deleted file mode 100644 index 5ca5ead51..000000000 --- a/API/DTOs/CopySettingsFromLibraryDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs; - -public sealed record CopySettingsFromLibraryDto -{ - public int SourceLibraryId { get; set; } - public List TargetLibraryIds { get; set; } - /// - /// Include copying over the type - /// - public bool IncludeType { get; set; } - -} diff --git a/API/DTOs/CoverDb/CoverDbAuthor.cs b/API/DTOs/CoverDb/CoverDbAuthor.cs deleted file mode 100644 index ca924801f..000000000 --- a/API/DTOs/CoverDb/CoverDbAuthor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using YamlDotNet.Serialization; - -namespace API.DTOs.CoverDb; - -public sealed record CoverDbAuthor -{ - [YamlMember(Alias = "name", ApplyNamingConventions = false)] - public string Name { get; set; } - [YamlMember(Alias = "aliases", ApplyNamingConventions = false)] - public List Aliases { get; set; } = new List(); - [YamlMember(Alias = "ids", ApplyNamingConventions = false)] - public CoverDbPersonIds Ids { get; set; } - [YamlMember(Alias = "image_path", ApplyNamingConventions = false)] - public string ImagePath { get; set; } -} diff --git a/API/DTOs/CoverDb/CoverDbPeople.cs b/API/DTOs/CoverDb/CoverDbPeople.cs deleted file mode 100644 index 2e825eac7..000000000 --- a/API/DTOs/CoverDb/CoverDbPeople.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using YamlDotNet.Serialization; - -namespace API.DTOs.CoverDb; - -public sealed record CoverDbPeople -{ - [YamlMember(Alias = "people", ApplyNamingConventions = false)] - public List People { get; set; } = new List(); -} diff --git a/API/DTOs/CoverDb/CoverDbPersonIds.cs b/API/DTOs/CoverDb/CoverDbPersonIds.cs deleted file mode 100644 index 5816bb479..000000000 --- a/API/DTOs/CoverDb/CoverDbPersonIds.cs +++ /dev/null @@ -1,20 +0,0 @@ -using YamlDotNet.Serialization; - -namespace API.DTOs.CoverDb; -#nullable enable - -public sealed record CoverDbPersonIds -{ - [YamlMember(Alias = "hardcover_id", ApplyNamingConventions = false)] - public string? HardcoverId { get; set; } = null; - [YamlMember(Alias = "amazon_id", ApplyNamingConventions = false)] - public string? AmazonId { get; set; } = null; - [YamlMember(Alias = "metron_id", ApplyNamingConventions = false)] - public string? MetronId { get; set; } = null; - [YamlMember(Alias = "comicvine_id", ApplyNamingConventions = false)] - public string? ComicVineId { get; set; } = null; - [YamlMember(Alias = "anilist_id", ApplyNamingConventions = false)] - public string? AnilistId { get; set; } = null; - [YamlMember(Alias = "mal_id", ApplyNamingConventions = false)] - public string? MALId { get; set; } = null; -} diff --git a/API/DTOs/Dashboard/DashboardStreamDto.cs b/API/DTOs/Dashboard/DashboardStreamDto.cs index 297a706b1..59e5f4f7d 100644 --- a/API/DTOs/Dashboard/DashboardStreamDto.cs +++ b/API/DTOs/Dashboard/DashboardStreamDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.Dashboard; -public sealed record DashboardStreamDto +public class DashboardStreamDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/Dashboard/GroupedSeriesDto.cs b/API/DTOs/Dashboard/GroupedSeriesDto.cs index 940e42c40..3b283de34 100644 --- a/API/DTOs/Dashboard/GroupedSeriesDto.cs +++ b/API/DTOs/Dashboard/GroupedSeriesDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Dashboard; /// /// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section /// -public sealed record GroupedSeriesDto +public class GroupedSeriesDto { public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } diff --git a/API/DTOs/Dashboard/RecentlyAddedItemDto.cs b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs index bb0360b30..2e5658e2e 100644 --- a/API/DTOs/Dashboard/RecentlyAddedItemDto.cs +++ b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Dashboard; /// /// A mesh of data for Recently added volume/chapters /// -public sealed record RecentlyAddedItemDto +public class RecentlyAddedItemDto { public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } diff --git a/API/DTOs/Dashboard/SmartFilterDto.cs b/API/DTOs/Dashboard/SmartFilterDto.cs index c1bc4d7e1..b23a74c69 100644 --- a/API/DTOs/Dashboard/SmartFilterDto.cs +++ b/API/DTOs/Dashboard/SmartFilterDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Dashboard; -public sealed record SmartFilterDto +public class SmartFilterDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs index 476a0732e..c2320f1a9 100644 --- a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs +++ b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Dashboard; -public sealed record UpdateDashboardStreamPositionDto +public class UpdateDashboardStreamPositionDto { public int FromPosition { get; set; } public int ToPosition { get; set; } diff --git a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs b/API/DTOs/Dashboard/UpdateStreamPositionDto.cs index 8de0ffa6f..f9005a585 100644 --- a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs +++ b/API/DTOs/Dashboard/UpdateStreamPositionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Dashboard; -public sealed record UpdateStreamPositionDto +public class UpdateStreamPositionDto { public int FromPosition { get; set; } public int ToPosition { get; set; } diff --git a/API/DTOs/DeleteChaptersDto.cs b/API/DTOs/DeleteChaptersDto.cs deleted file mode 100644 index 9fad2f1fb..000000000 --- a/API/DTOs/DeleteChaptersDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs; - -public sealed record DeleteChaptersDto -{ - public IList ChapterIds { get; set; } = default!; -} diff --git a/API/DTOs/DeleteSeriesDto.cs b/API/DTOs/DeleteSeriesDto.cs index ec9ba0c68..12687fc25 100644 --- a/API/DTOs/DeleteSeriesDto.cs +++ b/API/DTOs/DeleteSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public sealed record DeleteSeriesDto +public class DeleteSeriesDto { public IList SeriesIds { get; set; } = default!; } diff --git a/API/DTOs/Device/CreateDeviceDto.cs b/API/DTOs/Device/CreateDeviceDto.cs index a8fdb6bc9..7e59483fa 100644 --- a/API/DTOs/Device/CreateDeviceDto.cs +++ b/API/DTOs/Device/CreateDeviceDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums.Device; namespace API.DTOs.Device; -public sealed record CreateDeviceDto +public class CreateDeviceDto { [Required] public string Name { get; set; } = default!; diff --git a/API/DTOs/Device/DeviceDto.cs b/API/DTOs/Device/DeviceDto.cs index 42140dcc1..b2e83e6fc 100644 --- a/API/DTOs/Device/DeviceDto.cs +++ b/API/DTOs/Device/DeviceDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Device; /// /// A Device is an entity that can receive data from Kavita (kindle) /// -public sealed record DeviceDto +public class DeviceDto { /// /// The device Id diff --git a/API/DTOs/Device/SendSeriesToDeviceDto.cs b/API/DTOs/Device/SendSeriesToDeviceDto.cs index 58ce2293b..a0a907464 100644 --- a/API/DTOs/Device/SendSeriesToDeviceDto.cs +++ b/API/DTOs/Device/SendSeriesToDeviceDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Device; -public sealed record SendSeriesToDeviceDto +public class SendSeriesToDeviceDto { public int DeviceId { get; set; } public int SeriesId { get; set; } diff --git a/API/DTOs/Device/SendToDeviceDto.cs b/API/DTOs/Device/SendToDeviceDto.cs index a7a4dc0ff..fd88eaf59 100644 --- a/API/DTOs/Device/SendToDeviceDto.cs +++ b/API/DTOs/Device/SendToDeviceDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Device; -public sealed record SendToDeviceDto +public class SendToDeviceDto { public int DeviceId { get; set; } public IReadOnlyList ChapterIds { get; set; } = default!; diff --git a/API/DTOs/Device/UpdateDeviceDto.cs b/API/DTOs/Device/UpdateDeviceDto.cs index 2c3e72ea1..d28d372c3 100644 --- a/API/DTOs/Device/UpdateDeviceDto.cs +++ b/API/DTOs/Device/UpdateDeviceDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums.Device; namespace API.DTOs.Device; -public sealed record UpdateDeviceDto +public class UpdateDeviceDto { [Required] public int Id { get; set; } diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/API/DTOs/Downloads/DownloadBookmarkDto.cs index 00f763dac..5b7240b68 100644 --- a/API/DTOs/Downloads/DownloadBookmarkDto.cs +++ b/API/DTOs/Downloads/DownloadBookmarkDto.cs @@ -4,7 +4,7 @@ using API.DTOs.Reader; namespace API.DTOs.Downloads; -public sealed record DownloadBookmarkDto +public class DownloadBookmarkDto { [Required] public IEnumerable Bookmarks { get; set; } = default!; diff --git a/API/DTOs/Email/ConfirmationEmailDto.cs b/API/DTOs/Email/ConfirmationEmailDto.cs index 197395794..1a48c9974 100644 --- a/API/DTOs/Email/ConfirmationEmailDto.cs +++ b/API/DTOs/Email/ConfirmationEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public sealed record ConfirmationEmailDto +public class ConfirmationEmailDto { public string InvitingUser { get; init; } = default!; public string EmailAddress { get; init; } = default!; diff --git a/API/DTOs/Email/EmailHistoryDto.cs b/API/DTOs/Email/EmailHistoryDto.cs deleted file mode 100644 index c2968d091..000000000 --- a/API/DTOs/Email/EmailHistoryDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace API.DTOs.Email; - -public sealed record EmailHistoryDto -{ - public long Id { get; set; } - public bool Sent { get; set; } - public DateTime SendDate { get; set; } = DateTime.UtcNow; - public string EmailTemplate { get; set; } - public string ErrorMessage { get; set; } - public string ToUserName { get; set; } - -} diff --git a/API/DTOs/Email/EmailMigrationDto.cs b/API/DTOs/Email/EmailMigrationDto.cs index 5354afdaa..f051e7337 100644 --- a/API/DTOs/Email/EmailMigrationDto.cs +++ b/API/DTOs/Email/EmailMigrationDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public sealed record EmailMigrationDto +public class EmailMigrationDto { public string EmailAddress { get; init; } = default!; public string Username { get; init; } = default!; diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/API/DTOs/Email/EmailTestResultDto.cs index 9be868eab..6659e3a45 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/API/DTOs/Email/EmailTestResultDto.cs @@ -3,9 +3,8 @@ /// /// Represents if Test Email Service URL was successful or not and if any error occured /// -public sealed record EmailTestResultDto +public class EmailTestResultDto { public bool Successful { get; set; } public string ErrorMessage { get; set; } = default!; - public string EmailAddress { get; set; } = default!; } diff --git a/API/DTOs/Email/PasswordResetEmailDto.cs b/API/DTOs/Email/PasswordResetEmailDto.cs index 9fda066a9..06abba171 100644 --- a/API/DTOs/Email/PasswordResetEmailDto.cs +++ b/API/DTOs/Email/PasswordResetEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public sealed record PasswordResetEmailDto +public class PasswordResetEmailDto { public string EmailAddress { get; init; } = default!; public string ServerConfirmationLink { get; init; } = default!; diff --git a/API/DTOs/Email/SendToDto.cs b/API/DTOs/Email/SendToDto.cs index eacd29449..1261d110c 100644 --- a/API/DTOs/Email/SendToDto.cs +++ b/API/DTOs/Email/SendToDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Email; -public sealed record SendToDto +public class SendToDto { public string DestinationEmail { get; set; } = default!; public IEnumerable FilePaths { get; set; } = default!; diff --git a/API/DTOs/Email/TestEmailDto.cs b/API/DTOs/Email/TestEmailDto.cs index 44c11bd6c..37c12ed30 100644 --- a/API/DTOs/Email/TestEmailDto.cs +++ b/API/DTOs/Email/TestEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public sealed record TestEmailDto +public class TestEmailDto { public string Url { get; set; } = default!; } diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index cb3374838..1b8cffc9e 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -3,9 +3,8 @@ using API.Entities; using API.Entities.Enums; namespace API.DTOs.Filtering; -#nullable enable -public sealed record FilterDto +public class FilterDto { /// /// The type of Formats you want to be returned. An empty list will return all formats back diff --git a/API/DTOs/Filtering/LanguageDto.cs b/API/DTOs/Filtering/LanguageDto.cs index dde85f07e..bc7ebb5cc 100644 --- a/API/DTOs/Filtering/LanguageDto.cs +++ b/API/DTOs/Filtering/LanguageDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Filtering; -public sealed record LanguageDto +public class LanguageDto { public required string IsoCode { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Filtering/PersonSortField.cs b/API/DTOs/Filtering/PersonSortField.cs deleted file mode 100644 index 5268a1bf9..000000000 --- a/API/DTOs/Filtering/PersonSortField.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace API.DTOs.Filtering; - -public enum PersonSortField -{ - Name = 1, - SeriesCount = 2, - ChapterCount = 3 -} diff --git a/API/DTOs/Filtering/Range.cs b/API/DTOs/Filtering/Range.cs index e697f26e1..b9e9a5e49 100644 --- a/API/DTOs/Filtering/Range.cs +++ b/API/DTOs/Filtering/Range.cs @@ -1,10 +1,8 @@ namespace API.DTOs.Filtering; -#nullable enable - /// /// Represents a range between two int/float/double /// -public sealed record Range +public class Range { public T? Min { get; init; } public T? Max { get; init; } diff --git a/API/DTOs/Filtering/ReadStatus.cs b/API/DTOs/Filtering/ReadStatus.cs index 81498ecb5..eeb786714 100644 --- a/API/DTOs/Filtering/ReadStatus.cs +++ b/API/DTOs/Filtering/ReadStatus.cs @@ -3,7 +3,7 @@ /// /// Represents the Reading Status. This is a flag and allows multiple statues /// -public sealed record ReadStatus +public class ReadStatus { public bool NotRead { get; set; } = true; public bool InProgress { get; set; } = true; diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs index 7082ded69..f30b617df 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/API/DTOs/Filtering/SortField.cs @@ -30,12 +30,4 @@ public enum SortField /// Last time the user had any reading progress /// ReadProgress = 7, - /// - /// Kavita+ Only - External Average Rating - /// - AverageRating = 8, - /// - /// Randomise the order - /// - Random = 9 } diff --git a/API/DTOs/Filtering/SortOptions.cs b/API/DTOs/Filtering/SortOptions.cs index 18f2b17ea..00bf91675 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/API/DTOs/Filtering/SortOptions.cs @@ -3,17 +3,8 @@ /// /// Sorting Options for a query /// -public sealed record SortOptions +public class SortOptions { public SortField SortField { get; set; } public bool IsAscending { get; set; } = true; } - -/// -/// All Sorting Options for a query related to Person Entity -/// -public sealed record PersonSortOptions -{ - public PersonSortField SortField { get; set; } - public bool IsAscending { get; set; } = true; -} diff --git a/API/DTOs/Filtering/v2/DecodeFilterDto.cs b/API/DTOs/Filtering/v2/DecodeFilterDto.cs deleted file mode 100644 index db4c7ecce..000000000 --- a/API/DTOs/Filtering/v2/DecodeFilterDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace API.DTOs.Filtering.v2; - -/// -/// For requesting an encoded filter to be decoded -/// -public sealed record DecodeFilterDto -{ - public string EncodedFilter { get; set; } -} diff --git a/API/DTOs/Filtering/v2/FilterComparision.cs b/API/DTOs/Filtering/v2/FilterComparision.cs index 59bb86a8a..109667dad 100644 --- a/API/DTOs/Filtering/v2/FilterComparision.cs +++ b/API/DTOs/Filtering/v2/FilterComparision.cs @@ -53,8 +53,4 @@ public enum FilterComparison /// Is Date not between now and X seconds ago /// IsNotInLast = 15, - /// - /// There are no records - /// - IsEmpty = 16 } diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index 246a92a90..563c8e4a0 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -45,23 +45,5 @@ public enum FilterField /// Last time User Read /// ReadingDate = 27, - /// - /// Average rating from Kavita+ - Not usable for non-licensed users - /// - AverageRating = 28, - Imprint = 29, - Team = 30, - Location = 31, - /// - /// Last time User Read - /// - ReadLast = 32, -} -public enum PersonFilterField -{ - Role = 1, - Name = 2, - SeriesCount = 3, - ChapterCount = 4, } diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/API/DTOs/Filtering/v2/FilterStatementDto.cs index 8c99bd24c..a6192093e 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/API/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,17 +1,8 @@ -using API.DTOs.Metadata.Browse.Requests; +namespace API.DTOs.Filtering.v2; -namespace API.DTOs.Filtering.v2; - -public sealed record FilterStatementDto +public class FilterStatementDto { public FilterComparison Comparison { get; set; } public FilterField Field { get; set; } public string Value { get; set; } } - -public sealed record PersonFilterStatementDto -{ - public FilterComparison Comparison { get; set; } - public PersonFilterField Field { get; set; } - public string Value { get; set; } -} diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs index a247a17a6..e25f1e21d 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; + namespace API.DTOs.Filtering.v2; -#nullable enable + + /// /// Metadata filtering for v2 API only /// -public sealed record FilterV2Dto +public class FilterV2Dto { /// /// Not used in the UI. @@ -16,9 +18,9 @@ public sealed record FilterV2Dto /// The name of the filter /// public string? Name { get; set; } - public ICollection Statements { get; set; } = []; + public ICollection Statements { get; set; } = new List(); public FilterCombination Combination { get; set; } = FilterCombination.And; - public SortOptions? SortOptions { get; set; } + public SortOptions SortOptions { get; set; } /// /// Limit the number of rows returned. Defaults to not applying a limit (aka 0) diff --git a/API/DTOs/Jobs/JobDto.cs b/API/DTOs/Jobs/JobDto.cs index 55419811f..648765a34 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/API/DTOs/Jobs/JobDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Jobs; -public sealed record JobDto +public class JobDto { /// /// Job Id diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/API/DTOs/JumpBar/JumpKeyDto.cs index 8dc5b4a8e..5a98a85ca 100644 --- a/API/DTOs/JumpBar/JumpKeyDto.cs +++ b/API/DTOs/JumpBar/JumpKeyDto.cs @@ -3,7 +3,7 @@ /// /// Represents an individual button in a Jump Bar /// -public sealed record JumpKeyDto +public class JumpKeyDto { /// /// Number of items in this Key diff --git a/API/DTOs/KavitaLocale.cs b/API/DTOs/KavitaLocale.cs deleted file mode 100644 index 51868605f..000000000 --- a/API/DTOs/KavitaLocale.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace API.DTOs; - -public sealed record KavitaLocale -{ - public string FileName { get; set; } // Key - public string RenderName { get; set; } - public float TranslationCompletion { get; set; } - public bool IsRtL { get; set; } - public string Hash { get; set; } // ETAG hash so I can run my own localization busting implementation -} diff --git a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs b/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs deleted file mode 100644 index c053bd34e..000000000 --- a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace API.DTOs.KavitaPlus.Account; - -public sealed record AniListUpdateDto -{ - public string Token { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs b/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs deleted file mode 100644 index 340ad0f4c..000000000 --- a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace API.DTOs.KavitaPlus.Account; - -/// -/// Represents information around a user's tokens and their status -/// -public sealed record UserTokenInfo -{ - public int UserId { get; set; } - public string Username { get; set; } - public bool IsAniListTokenSet { get; set; } - public bool IsAniListTokenValid { get; set; } - public DateTime AniListValidUntilUtc { get; set; } - public bool IsMalTokenSet { get; set; } -} diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs deleted file mode 100644 index c05ff0567..000000000 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -using API.DTOs.Scrobbling; - -namespace API.DTOs.KavitaPlus.ExternalMetadata; -#nullable enable - -/// -/// Used for matching and fetching metadata on a series -/// -public sealed record ExternalMetadataIdsDto -{ - public long? MalId { get; set; } - public int? AniListId { get; set; } - - public string? SeriesName { get; set; } - public string? LocalizedSeriesName { get; set; } - public PlusMediaFormat? PlusMediaFormat { get; set; } = DTOs.Scrobbling.PlusMediaFormat.Unknown; -} diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs deleted file mode 100644 index a7359d69b..000000000 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.Scrobbling; - -namespace API.DTOs.KavitaPlus.ExternalMetadata; -#nullable enable - -/// -/// Represents a request to match some series from Kavita to an external id which K+ uses. -/// -public sealed record MatchSeriesRequestDto -{ - public required string SeriesName { get; set; } - public ICollection AlternativeNames { get; set; } = []; - public int Year { get; set; } = 0; - public string? Query { get; set; } - public int? AniListId { get; set; } - public long? MalId { get; set; } - public string? HardcoverId { get; set; } - public int? CbrId { get; set; } - public PlusMediaFormat Format { get; set; } -} diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs deleted file mode 100644 index 84e9bbf3e..000000000 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; - -namespace API.DTOs.KavitaPlus.ExternalMetadata; - -public sealed record SeriesDetailPlusApiDto -{ - public IEnumerable Recommendations { get; set; } - public IEnumerable Reviews { get; set; } - public IEnumerable Ratings { get; set; } - public ExternalSeriesDetailDto? Series { get; set; } - public int? AniListId { get; set; } - public long? MalId { get; set; } - public int? CbrId { get; set; } -} diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs deleted file mode 100644 index dd85dd063..000000000 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace API.DTOs.KavitaPlus.License; -#nullable enable - -public sealed record EncryptLicenseDto -{ - public required string License { get; set; } - public required string InstallId { get; set; } - public required string EmailId { get; set; } - public string? DiscordId { get; set; } -} diff --git a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs b/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs deleted file mode 100644 index 2cd9b5896..000000000 --- a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace API.DTOs.KavitaPlus.License; - -public sealed record LicenseInfoDto -{ - /// - /// If cancelled, will represent cancellation date. If not, will represent repayment date - /// - public DateTime ExpirationDate { get; set; } - /// - /// If cancelled or not - /// - public bool IsActive { get; set; } - /// - /// If will be or is cancelled - /// - public bool IsCancelled { get; set; } - /// - /// Is the installed version valid for Kavita+ (aka within 3 releases) - /// - public bool IsValidVersion { get; set; } - /// - /// The email on file - /// - public string RegisteredEmail { get; set; } - /// - /// Number of months user has been subscribed - /// - public int TotalMonthsSubbed { get; set; } - /// - /// A license is stored within Kavita - /// - public bool HasLicense { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs deleted file mode 100644 index c394cf8d4..000000000 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace API.DTOs.KavitaPlus.Manage; - -/// -/// Represents an option in the UI layer for Filtering -/// -public enum MatchStateOption -{ - All = 0, - Matched = 1, - NotMatched = 2, - Error = 3, - DontMatch = 4 -} - -public sealed record ManageMatchFilterDto -{ - public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All; - /// - /// Library Type in int form. -1 indicates to ignore the field. - /// - public int LibraryType { get; set; } = -1; - public string SearchTerm { get; set; } = string.Empty; -} diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs deleted file mode 100644 index a51e63ee9..000000000 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace API.DTOs.KavitaPlus.Manage; - -public sealed record ManageMatchSeriesDto -{ - public SeriesDto Series { get; set; } - public bool IsMatched { get; set; } - public DateTime ValidUntilUtc { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs deleted file mode 100644 index add9ca723..000000000 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using API.DTOs.SeriesDetail; - -namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable - -/// -/// Information about an individual issue/chapter/book from Kavita+ -/// -public sealed record ExternalChapterDto -{ - public string Title { get; set; } - - public string IssueNumber { get; set; } - - public decimal? CriticRating { get; set; } - - public decimal? UserRating { get; set; } - - public string? Summary { get; set; } - - public IList? Writers { get; set; } - - public IList? Artists { get; set; } - - public DateTime? ReleaseDate { get; set; } - - public string? Publisher { get; set; } - - public string? CoverImageUrl { get; set; } - - public string? IssueUrl { get; set; } - - public IList CriticReviews { get; set; } - public IList UserReviews { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs deleted file mode 100644 index 6704bf697..000000000 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Services.Plus; - -namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable - -/// -/// This is AniListSeries -/// -public sealed record ExternalSeriesDetailDto -{ - public string Name { get; set; } - public int? AniListId { get; set; } - public long? MALId { get; set; } - public int? CbrId { get; set; } - public IList Synonyms { get; set; } = []; - public PlusMediaFormat PlusMediaFormat { get; set; } - public string? SiteUrl { get; set; } - public string? CoverUrl { get; set; } - public IList Genres { get; set; } - public IList Staff { get; set; } - public IList Tags { get; set; } - public string? Summary { get; set; } - public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList; - - public DateTime? StartDate { get; set; } - public DateTime? EndDate { get; set; } - public int AverageScore { get; set; } - /// AniList returns the total count of unique chapters, includes 1.1 for example - public int Chapters { get; set; } - /// AniList returns the total count of unique volumes, includes 1.1 for example - public int Volumes { get; set; } - public IList? Relations { get; set; } = []; - public IList? Characters { get; set; } = []; - - #region Comic Only - public string? Publisher { get; set; } - /// - /// Only from CBR for . Full metadata about issues - /// - public IList? ChapterDtos { get; set; } - #endregion - - -} diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs deleted file mode 100644 index a9debabd1..000000000 --- a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using API.Entities.Enums; - -namespace API.DTOs.KavitaPlus.Metadata; - -public sealed record MetadataFieldMappingDto -{ - public int Id { get; set; } - public MetadataFieldType SourceType { get; set; } - public MetadataFieldType DestinationType { get; set; } - /// - /// The string in the source - /// - public string SourceValue { get; set; } - /// - /// Write the string as this in the Destination (can also just be the Source) - /// - public string DestinationValue { get; set; } - /// - /// If true, the tag will be Moved over vs Copied over - /// - public bool ExcludeFromSource { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs deleted file mode 100644 index e9f6614bc..000000000 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using NotImplementedException = System.NotImplementedException; - -namespace API.DTOs.KavitaPlus.Metadata; - - -public sealed record MetadataSettingsDto -{ - /// - /// If writing any sort of metadata from upstream (AniList, Hardcover) source is allowed - /// - public bool Enabled { get; set; } - - /// - /// Allow the Summary to be written - /// - public bool EnableSummary { get; set; } - /// - /// Allow Publication status to be derived and updated - /// - public bool EnablePublicationStatus { get; set; } - /// - /// Allow Relationships between series to be set - /// - public bool EnableRelationships { get; set; } - /// - /// Allow People to be created (including downloading images) - /// - public bool EnablePeople { get; set; } - /// - /// Allow Start date to be set within the Series - /// - public bool EnableStartDate { get; set; } - /// - /// Allow setting the Localized name - /// - public bool EnableLocalizedName { get; set; } - /// - /// Allow setting the cover image - /// - public bool EnableCoverImage { get; set; } - - #region Chapter Metadata - /// - /// Allow Summary to be set within Chapter/Issue - /// - public bool EnableChapterSummary { get; set; } - /// - /// Allow Release Date to be set within Chapter/Issue - /// - public bool EnableChapterReleaseDate { get; set; } - /// - /// Allow Title to be set within Chapter/Issue - /// - public bool EnableChapterTitle { get; set; } - /// - /// Allow Publisher to be set within Chapter/Issue - /// - public bool EnableChapterPublisher { get; set; } - /// - /// Allow setting the cover image for the Chapter/Issue - /// - public bool EnableChapterCoverImage { get; set; } - #endregion - - // Need to handle the Genre/tags stuff - public bool EnableGenres { get; set; } = true; - public bool EnableTags { get; set; } = true; - - /// - /// For Authors and Writers, how should names be stored (Exclusively applied for AniList). This does not affect Character names. - /// - public bool FirstLastPeopleNaming { get; set; } - - /// - /// Any Genres or Tags that if present, will trigger an Age Rating Override. Highest rating will be prioritized for matching. - /// - public Dictionary AgeRatingMappings { get; set; } - - /// - /// A list of rules that allow mapping a genre/tag to another genre/tag - /// - public List FieldMappings { get; set; } - /// - /// A list of overrides that will enable writing to locked fields - /// - public List Overrides { get; set; } - - /// - /// Do not allow any Genre/Tag in this list to be written to Kavita - /// - public List Blacklist { get; set; } - /// - /// Only allow these Tags to be written to Kavita - /// - public List Whitelist { get; set; } - /// - /// Which Roles to allow metadata downloading for - /// - public List PersonRoles { get; set; } - - - /// - /// Override list contains this field - /// - /// - /// - public bool HasOverride(MetadataSettingField field) - { - return Overrides.Contains(field); - } - - /// - /// If this Person role is allowed to be written - /// - /// - /// - public bool IsPersonAllowed(PersonRole character) - { - return PersonRoles.Contains(character); - } -} diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs deleted file mode 100644 index 2b57548cd..000000000 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable - -public enum CharacterRole -{ - Main = 0, - Supporting = 1, - Background = 2 -} - - -public sealed record SeriesCharacter -{ - public string Name { get; set; } - public required string Description { get; set; } - public required string Url { get; set; } - public string? ImageUrl { get; set; } - public CharacterRole Role { get; set; } -} diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs b/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs deleted file mode 100644 index 0b1f619a2..000000000 --- a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs +++ /dev/null @@ -1,24 +0,0 @@ -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Services.Plus; - -namespace API.DTOs.KavitaPlus.Metadata; - -public sealed record ALMediaTitle -{ - public string? EnglishTitle { get; set; } - public string RomajiTitle { get; set; } - public string NativeTitle { get; set; } - public string PreferredTitle { get; set; } -} - -public sealed record SeriesRelationship -{ - public int AniListId { get; set; } - public int? MalId { get; set; } - public ALMediaTitle SeriesName { get; set; } - public RelationKind Relation { get; set; } - public ScrobbleProvider Provider { get; set; } - public PlusMediaFormat PlusMediaFormat { get; set; } = PlusMediaFormat.Manga; -} diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/API/DTOs/Koreader/KoreaderBookDto.cs deleted file mode 100644 index b66b7da3a..000000000 --- a/API/DTOs/Koreader/KoreaderBookDto.cs +++ /dev/null @@ -1,33 +0,0 @@ -using API.DTOs.Progress; - -namespace API.DTOs.Koreader; - -/// -/// This is the interface for receiving and sending updates to Koreader. The only fields -/// that are actually used are the Document and Progress fields. -/// -public class KoreaderBookDto -{ - /// - /// This is the Koreader hash of the book. It is used to identify the book. - /// - public string Document { get; set; } - /// - /// A randomly generated id from the koreader device. Only used to maintain the Koreader interface. - /// - public string Device_id { get; set; } - /// - /// The Koreader device name. Only used to maintain the Koreader interface. - /// - public string Device { get; set; } - /// - /// Percent progress of the book. Only used to maintain the Koreader interface. - /// - public float Percentage { get; set; } - /// - /// An XPath string read by Koreader to determine the location within the epub. - /// Essentially, it is Koreader's equivalent to ProgressDto.BookScrollId. - /// - /// - public string Progress { get; set; } -} diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs deleted file mode 100644 index 52a1d6cbd..000000000 --- a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace API.DTOs.Koreader; - -public class KoreaderProgressUpdateDto -{ - /// - /// This is the Koreader hash of the book. It is used to identify the book. - /// - public string Document { get; set; } - /// - /// UTC Timestamp to return to KOReader - /// - public DateTime Timestamp { get; set; } -} diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index bd72ad2f0..7c1d453b8 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using API.Entities.Enums; namespace API.DTOs; -#nullable enable -public sealed record LibraryDto +public class LibraryDto { public int Id { get; init; } public string? Name { get; init; } @@ -52,26 +51,4 @@ public sealed record LibraryDto /// When showing series, only parent series or series with no relationships will be returned /// public bool CollapseSeriesRelationships { get; set; } = false; - /// - /// The types of file type groups the library will scan for - /// - public ICollection LibraryFileTypes { get; set; } - /// - /// A set of globs that will exclude matching content from being scanned - /// - public ICollection ExcludePatterns { get; set; } - /// - /// Allow any series within this Library to download metadata. - /// - /// This does not exclude the library from being linked to wrt Series Relationships - /// Requires a valid LicenseKey - public bool AllowMetadataMatching { get; set; } = true; - /// - /// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF) - /// - public bool EnableMetadata { get; set; } = true; - /// - /// Should Kavita remove sort articles "The" for the sort name - /// - public bool RemovePrefixForSortName { get; set; } = false; } diff --git a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs b/API/DTOs/License/EncryptLicenseDto.cs similarity index 66% rename from API/DTOs/KavitaPlus/License/ResetLicenseDto.cs rename to API/DTOs/License/EncryptLicenseDto.cs index d0fd9b666..df4426018 100644 --- a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs +++ b/API/DTOs/License/EncryptLicenseDto.cs @@ -1,6 +1,6 @@ -namespace API.DTOs.KavitaPlus.License; +namespace API.DTOs.License; -public sealed record ResetLicenseDto +public class EncryptLicenseDto { public required string License { get; set; } public required string InstallId { get; set; } diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/API/DTOs/License/UpdateLicenseDto.cs similarity index 55% rename from API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs rename to API/DTOs/License/UpdateLicenseDto.cs index 28b47efbe..1b4270e6b 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/API/DTOs/License/UpdateLicenseDto.cs @@ -1,7 +1,6 @@ -namespace API.DTOs.KavitaPlus.License; -#nullable enable +namespace API.DTOs.License; -public sealed record UpdateLicenseDto +public class UpdateLicenseDto { /// /// License Key received from Kavita+ @@ -11,8 +10,4 @@ public sealed record UpdateLicenseDto /// Email registered with Stripe /// public required string Email { get; set; } - /// - /// Optional DiscordId - /// - public string? DiscordId { get; set; } } diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 23bb37467..9be3c117f 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -2,28 +2,14 @@ using API.Entities.Enums; namespace API.DTOs; -#nullable enable -public sealed record MangaFileDto +public class MangaFileDto { public int Id { get; init; } - /// - /// Absolute path to the archive file (normalized) - /// public string FilePath { get; init; } = default!; - /// - /// Number of pages for the given file - /// public int Pages { get; init; } - /// - /// How many bytes make up this file - /// public long Bytes { get; init; } public MangaFormat Format { get; init; } public DateTime Created { get; init; } - /// - /// File extension - /// - public string? Extension { get; set; } } diff --git a/API/DTOs/MediaErrors/MediaErrorDto.cs b/API/DTOs/MediaErrors/MediaErrorDto.cs index b77ee88be..d890108d2 100644 --- a/API/DTOs/MediaErrors/MediaErrorDto.cs +++ b/API/DTOs/MediaErrors/MediaErrorDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.MediaErrors; -public sealed record MediaErrorDto +public class MediaErrorDto { /// /// Format Type (RAR, ZIP, 7Zip, Epub, PDF) @@ -20,6 +20,4 @@ public sealed record MediaErrorDto /// Exception message /// public string Details { get; set; } - - public DateTime CreatedUtc { get; set; } } diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index f5f24b284..31b5e62be 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -3,12 +3,11 @@ using System.Collections.Generic; using API.DTOs.Account; namespace API.DTOs; -#nullable enable /// /// Represents a member of a Kavita server. /// -public sealed record MemberDto +public class MemberDto { public int Id { get; init; } public string? Username { get; init; } @@ -19,9 +18,7 @@ public sealed record MemberDto public bool IsPending { get; init; } public AgeRestrictionDto? AgeRestriction { get; init; } public DateTime Created { get; init; } - public DateTime CreatedUtc { get; init; } public DateTime LastActive { get; init; } - public DateTime LastActiveUtc { get; init; } public IEnumerable? Libraries { get; init; } public IEnumerable? Roles { get; init; } } diff --git a/API/DTOs/Metadata/AgeRatingDto.cs b/API/DTOs/Metadata/AgeRatingDto.cs index bfa835ef5..07523c3fe 100644 --- a/API/DTOs/Metadata/AgeRatingDto.cs +++ b/API/DTOs/Metadata/AgeRatingDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Metadata; -public sealed record AgeRatingDto +public class AgeRatingDto { public AgeRating Value { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs deleted file mode 100644 index 8044c7914..000000000 --- a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs.Metadata.Browse; - -public sealed record BrowseGenreDto : GenreTagDto -{ - /// - /// Number of Series this Entity is on - /// - public int SeriesCount { get; set; } - /// - /// Number of Chapters this Entity is on - /// - public int ChapterCount { get; set; } -} diff --git a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs b/API/DTOs/Metadata/Browse/BrowsePersonDto.cs deleted file mode 100644 index 20f84b783..000000000 --- a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using API.DTOs.Person; - -namespace API.DTOs.Metadata.Browse; - -/// -/// Used to browse writers and click in to see their series -/// -public class BrowsePersonDto : PersonDto -{ - /// - /// Number of Series this Person is the Writer for - /// - public int SeriesCount { get; set; } - /// - /// Number of Issues this Person is the Writer for - /// - public int ChapterCount { get; set; } -} diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/API/DTOs/Metadata/Browse/BrowseTagDto.cs deleted file mode 100644 index 9a71876e3..000000000 --- a/API/DTOs/Metadata/Browse/BrowseTagDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs.Metadata.Browse; - -public sealed record BrowseTagDto : TagDto -{ - /// - /// Number of Series this Entity is on - /// - public int SeriesCount { get; set; } - /// - /// Number of Chapters this Entity is on - /// - public int ChapterCount { get; set; } -} diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs deleted file mode 100644 index d41cf37f3..000000000 --- a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; - -namespace API.DTOs.Metadata.Browse.Requests; -#nullable enable - -public sealed record BrowsePersonFilterDto -{ - /// - /// Not used - For parity with Series Filter - /// - public int Id { get; set; } - /// - /// Not used - For parity with Series Filter - /// - public string? Name { get; set; } - public ICollection Statements { get; set; } = []; - public FilterCombination Combination { get; set; } = FilterCombination.And; - public PersonSortOptions? SortOptions { get; set; } - - /// - /// Limit the number of rows returned. Defaults to not applying a limit (aka 0) - /// - public int LimitTo { get; set; } = 0; -} diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/API/DTOs/Metadata/ChapterMetadataDto.cs index c79436e24..b9b04cfac 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/API/DTOs/Metadata/ChapterMetadataDto.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Person; +using System.Collections.Generic; using API.Entities.Enums; namespace API.DTOs.Metadata; -#nullable enable /// /// Exclusively metadata about a given chapter /// -[Obsolete("Will not be maintained as of v0.8.1")] -public sealed record ChapterMetadataDto +public class ChapterMetadataDto { public int Id { get; set; } public int ChapterId { get; set; } @@ -21,13 +17,10 @@ public sealed record ChapterMetadataDto public ICollection Characters { get; set; } = new List(); public ICollection Pencillers { get; set; } = new List(); public ICollection Inkers { get; set; } = new List(); - public ICollection Imprints { get; set; } = new List(); public ICollection Colorists { get; set; } = new List(); public ICollection Letterers { get; set; } = new List(); public ICollection Editors { get; set; } = new List(); public ICollection Translators { get; set; } = new List(); - public ICollection Teams { get; set; } = new List(); - public ICollection Locations { get; set; } = new List(); public ICollection Genres { get; set; } = new List(); diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs index 13a339d38..cf05ebbff 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/API/DTOs/Metadata/GenreTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public record GenreTagDto +public class GenreTagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs b/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs deleted file mode 100644 index 774581b37..000000000 --- a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; - -namespace API.DTOs.Metadata.Matching; - -public sealed record ExternalSeriesMatchDto -{ - public ExternalSeriesDetailDto Series { get; set; } - public float MatchRating { get; set; } -} diff --git a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs b/API/DTOs/Metadata/Matching/MatchSeriesDto.cs deleted file mode 100644 index bb497b9ab..000000000 --- a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace API.DTOs.Metadata.Matching; - -/// -/// Used for matching a series with Kavita+ for metadata and scrobbling -/// -public sealed record MatchSeriesDto -{ - /// - /// When set, Kavita will stop attempting to match this series and will not perform any scrobbling - /// - public bool DontMatch { get; set; } - /// - /// Series Id to pull internal metadata from to improve matching - /// - public int SeriesId { get; set; } - /// - /// Free form text to query for. Can be a url and ids will be parsed from it - /// - public string Query { get; set; } -} diff --git a/API/DTOs/Metadata/PublicationStatusDto.cs b/API/DTOs/Metadata/PublicationStatusDto.cs index b4f12500a..b8166a6e5 100644 --- a/API/DTOs/Metadata/PublicationStatusDto.cs +++ b/API/DTOs/Metadata/PublicationStatusDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Metadata; -public sealed record PublicationStatusDto +public class PublicationStatusDto { public PublicationStatus Value { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs index f5c925e1f..59e03a279 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/API/DTOs/Metadata/TagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public record TagDto +public class TagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/OPDS/Feed.cs b/API/DTOs/OPDS/Feed.cs index 5f4c4b115..76a740b89 100644 --- a/API/DTOs/OPDS/Feed.cs +++ b/API/DTOs/OPDS/Feed.cs @@ -4,13 +4,11 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; -// TODO: OPDS Dtos are internal state, shouldn't be in DTO directory - /// /// /// [XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")] -public sealed record Feed +public class Feed { [XmlElement("updated")] public string Updated { get; init; } = DateTime.UtcNow.ToString("s"); diff --git a/API/DTOs/OPDS/FeedAuthor.cs b/API/DTOs/OPDS/FeedAuthor.cs index 4196997dd..1fd3e6cd2 100644 --- a/API/DTOs/OPDS/FeedAuthor.cs +++ b/API/DTOs/OPDS/FeedAuthor.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public sealed record FeedAuthor +public class FeedAuthor { [XmlElement("name")] public string Name { get; set; } diff --git a/API/DTOs/OPDS/FeedCategory.cs b/API/DTOs/OPDS/FeedCategory.cs index 2352b4af2..3129fab60 100644 --- a/API/DTOs/OPDS/FeedCategory.cs +++ b/API/DTOs/OPDS/FeedCategory.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public sealed record FeedCategory +public class FeedCategory { [XmlAttribute("scheme")] public string Scheme { get; } = "http://www.bisg.org/standards/bisac_subject/index.html"; diff --git a/API/DTOs/OPDS/FeedEntry.cs b/API/DTOs/OPDS/FeedEntry.cs index 838ebd124..da8b53b74 100644 --- a/API/DTOs/OPDS/FeedEntry.cs +++ b/API/DTOs/OPDS/FeedEntry.cs @@ -5,7 +5,7 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; #nullable enable -public sealed record FeedEntry +public class FeedEntry { [XmlElement("updated")] public string Updated { get; init; } = DateTime.UtcNow.ToString("s"); diff --git a/API/DTOs/OPDS/FeedEntryContent.cs b/API/DTOs/OPDS/FeedEntryContent.cs index 4de9b73bd..3e95ce643 100644 --- a/API/DTOs/OPDS/FeedEntryContent.cs +++ b/API/DTOs/OPDS/FeedEntryContent.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public sealed record FeedEntryContent +public class FeedEntryContent { [XmlAttribute("type")] public string Type = "text"; diff --git a/API/DTOs/OPDS/FeedLink.cs b/API/DTOs/OPDS/FeedLink.cs index 28c55bbe8..2a9053f16 100644 --- a/API/DTOs/OPDS/FeedLink.cs +++ b/API/DTOs/OPDS/FeedLink.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; -public sealed record FeedLink +public class FeedLink { [XmlIgnore] public bool IsPageStream { get; set; } @@ -39,7 +39,7 @@ public sealed record FeedLink /// /// Attribute MUST conform Atom's Date construct [XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")] - public string LastReadDate { get; set; } + public DateTime LastReadDate { get; set; } public bool ShouldSerializeLastReadDate() { diff --git a/API/DTOs/OPDS/OpenSearchDescription.cs b/API/DTOs/OPDS/OpenSearchDescription.cs index eba26572f..cc8392a88 100644 --- a/API/DTOs/OPDS/OpenSearchDescription.cs +++ b/API/DTOs/OPDS/OpenSearchDescription.cs @@ -3,7 +3,7 @@ namespace API.DTOs.OPDS; [XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")] -public sealed record OpenSearchDescription +public class OpenSearchDescription { /// /// Contains a brief human-readable title that identifies this search engine. diff --git a/API/DTOs/OPDS/SearchLink.cs b/API/DTOs/OPDS/SearchLink.cs index b4698c221..dba67f3bd 100644 --- a/API/DTOs/OPDS/SearchLink.cs +++ b/API/DTOs/OPDS/SearchLink.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public sealed record SearchLink +public class SearchLink { [XmlAttribute("type")] public string Type { get; set; } = default!; diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs deleted file mode 100644 index db152e3b1..000000000 --- a/API/DTOs/Person/PersonDto.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs.Person; -#nullable enable - -public class PersonDto -{ - public int Id { get; set; } - public required string Name { get; set; } - - public bool CoverImageLocked { get; set; } - public string? PrimaryColor { get; set; } - public string? SecondaryColor { get; set; } - - public string? CoverImage { get; set; } - public List Aliases { get; set; } = []; - - public string? Description { get; set; } - /// - /// ASIN for person - /// - /// Can be used for Amazon author lookup - public string? Asin { get; set; } - - /// - /// https://anilist.co/staff/{AniListId}/ - /// - /// Kavita+ Only - public int AniListId { get; set; } = 0; - /// - /// https://myanimelist.net/people/{MalId}/ - /// https://myanimelist.net/character/{MalId}/CharacterName - /// - /// Kavita+ Only - public long MalId { get; set; } = 0; - /// - /// https://hardcover.app/authors/{HardcoverId} - /// - /// Kavita+ Only - public string? HardcoverId { get; set; } -} diff --git a/API/DTOs/Person/PersonMergeDto.cs b/API/DTOs/Person/PersonMergeDto.cs deleted file mode 100644 index b5dc23375..000000000 --- a/API/DTOs/Person/PersonMergeDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs; - -public sealed record PersonMergeDto -{ - /// - /// The id of the person being merged into - /// - [Required] - public int DestId { get; init; } - /// - /// The id of the person being merged. This person will be removed, and become an alias of - /// - [Required] - public int SrcId { get; init; } -} diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs deleted file mode 100644 index b43a45e88..000000000 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs; -#nullable enable - -public sealed record UpdatePersonDto -{ - [Required] - public int Id { get; init; } - [Required] - public bool CoverImageLocked { get; set; } - [Required] - public string Name {get; set;} - public IList Aliases { get; set; } = []; - public string? Description { get; set; } - - public int? AniListId { get; set; } - public long? MalId { get; set; } - public string? HardcoverId { get; set; } - public string? Asin { get; set; } -} diff --git a/API/DTOs/PersonDto.cs b/API/DTOs/PersonDto.cs new file mode 100644 index 000000000..85cc72bb0 --- /dev/null +++ b/API/DTOs/PersonDto.cs @@ -0,0 +1,10 @@ +using API.Entities.Enums; + +namespace API.DTOs; + +public class PersonDto +{ + public int Id { get; set; } + public required string Name { get; set; } + public PersonRole Role { get; set; } +} diff --git a/API/DTOs/Progress/FullProgressDto.cs b/API/DTOs/Progress/FullProgressDto.cs deleted file mode 100644 index 4f97ab44a..000000000 --- a/API/DTOs/Progress/FullProgressDto.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace API.DTOs.Progress; - -/// -/// A full progress Record from the DB (not all data, only what's needed for API) -/// -public sealed record FullProgressDto -{ - public int Id { get; set; } - public int ChapterId { get; set; } - public int PagesRead { get; set; } - public DateTime LastModified { get; set; } - public DateTime LastModifiedUtc { get; set; } - public DateTime Created { get; set; } - public DateTime CreatedUtc { get; set; } - public int AppUserId { get; set; } - public string UserName { get; set; } -} diff --git a/API/DTOs/Progress/ProgressDto.cs b/API/DTOs/ProgressDto.cs similarity index 91% rename from API/DTOs/Progress/ProgressDto.cs rename to API/DTOs/ProgressDto.cs index 0add848c5..2a05360c4 100644 --- a/API/DTOs/Progress/ProgressDto.cs +++ b/API/DTOs/ProgressDto.cs @@ -1,10 +1,10 @@ using System; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Progress; +namespace API.DTOs; #nullable enable -public sealed record ProgressDto +public class ProgressDto { [Required] public int VolumeId { get; set; } diff --git a/API/DTOs/RatingDto.cs b/API/DTOs/RatingDto.cs index 101aa7ac5..89f4aebe5 100644 --- a/API/DTOs/RatingDto.cs +++ b/API/DTOs/RatingDto.cs @@ -1,18 +1,11 @@ -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Services.Plus; +using API.Services.Plus; namespace API.DTOs; -#nullable enable -public sealed record RatingDto +public class RatingDto { - public int AverageScore { get; set; } public int FavoriteCount { get; set; } public ScrobbleProvider Provider { get; set; } - /// - public RatingAuthority Authority { get; set; } = RatingAuthority.User; public string? ProviderUrl { get; set; } } diff --git a/API/DTOs/Reader/BookChapterItem.cs b/API/DTOs/Reader/BookChapterItem.cs index 892e82e27..dcfb7b904 100644 --- a/API/DTOs/Reader/BookChapterItem.cs +++ b/API/DTOs/Reader/BookChapterItem.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public sealed record BookChapterItem +public class BookChapterItem { /// /// Name of the Chapter diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index 2473cd5dc..c379f71f8 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public sealed record BookInfoDto : IChapterInfoDto +public class BookInfoDto : IChapterInfoDto { public string BookTitle { get; set; } = default! ; public int SeriesId { get; set; } diff --git a/API/DTOs/Reader/BookmarkDto.cs b/API/DTOs/Reader/BookmarkDto.cs index da18fc28e..a6b185683 100644 --- a/API/DTOs/Reader/BookmarkDto.cs +++ b/API/DTOs/Reader/BookmarkDto.cs @@ -1,9 +1,8 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Reader; -#nullable enable -public sealed record BookmarkDto +public class BookmarkDto { public int Id { get; set; } [Required] diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/API/DTOs/Reader/BookmarkInfoDto.cs index c75c3d8bf..7583ee76d 100644 --- a/API/DTOs/Reader/BookmarkInfoDto.cs +++ b/API/DTOs/Reader/BookmarkInfoDto.cs @@ -2,7 +2,6 @@ using API.Entities.Enums; namespace API.DTOs.Reader; -#nullable enable public class BookmarkInfoDto { diff --git a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs index 51ccf5cc3..7490f837c 100644 --- a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs +++ b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public sealed record BulkRemoveBookmarkForSeriesDto +public class BulkRemoveBookmarkForSeriesDto { public ICollection SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 4da08a31d..36ddd554e 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -2,12 +2,11 @@ using API.Entities.Enums; namespace API.DTOs.Reader; -#nullable enable /// /// Information about the Chapter for the Reader to render /// -public sealed record ChapterInfoDto : IChapterInfoDto +public class ChapterInfoDto : IChapterInfoDto { /// /// The Chapter Number @@ -66,14 +65,6 @@ public sealed record ChapterInfoDto : IChapterInfoDto /// /// Usually just series name, but can include chapter title public string Title { get; set; } = default!; - /// - /// Total pages for the series - /// - public int SeriesTotalPages { get; set; } - /// - /// Total pages read for the series - /// - public int SeriesTotalPagesRead { get; set; } /// /// List of all files with their inner archive structure maintained in filename and dimensions @@ -85,4 +76,5 @@ public sealed record ChapterInfoDto : IChapterInfoDto /// /// This is optionally returned by includeDimensions public IDictionary? DoublePairs { get; set; } + } diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs index 95272ca58..25526b490 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/API/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,7 +1,6 @@ namespace API.DTOs.Reader; -#nullable enable -public sealed record CreatePersonalToCDto +public class CreatePersonalToCDto { public required int ChapterId { get; set; } public required int VolumeId { get; set; } diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/API/DTOs/Reader/FileDimensionDto.cs index 7a7d2978f..baee20dd0 100644 --- a/API/DTOs/Reader/FileDimensionDto.cs +++ b/API/DTOs/Reader/FileDimensionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public sealed record FileDimensionDto +public class FileDimensionDto { public int Width { get; set; } public int Height { get; set; } diff --git a/API/DTOs/Reader/HourEstimateRangeDto.cs b/API/DTOs/Reader/HourEstimateRangeDto.cs index 3facf8e56..4343e2e93 100644 --- a/API/DTOs/Reader/HourEstimateRangeDto.cs +++ b/API/DTOs/Reader/HourEstimateRangeDto.cs @@ -3,7 +3,7 @@ /// /// A range of time to read a selection (series, chapter, etc) /// -public sealed record HourEstimateRangeDto +public record HourEstimateRangeDto { /// /// Min hours to read the selection @@ -16,5 +16,5 @@ public sealed record HourEstimateRangeDto /// /// Estimated average hours to read the selection /// - public float AvgHours { get; init; } = 1f; + public int AvgHours { get; init; } = 1; } diff --git a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs index 4c39f7d76..50187ec81 100644 --- a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs +++ b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public sealed record MarkMultipleSeriesAsReadDto +public class MarkMultipleSeriesAsReadDto { public IReadOnlyList SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/MarkReadDto.cs b/API/DTOs/Reader/MarkReadDto.cs index c6f7367c0..9bf46a6d5 100644 --- a/API/DTOs/Reader/MarkReadDto.cs +++ b/API/DTOs/Reader/MarkReadDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public sealed record MarkReadDto +public class MarkReadDto { public int SeriesId { get; init; } } diff --git a/API/DTOs/Reader/MarkVolumeReadDto.cs b/API/DTOs/Reader/MarkVolumeReadDto.cs index be95d2e98..47ffd2649 100644 --- a/API/DTOs/Reader/MarkVolumeReadDto.cs +++ b/API/DTOs/Reader/MarkVolumeReadDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public sealed record MarkVolumeReadDto +public class MarkVolumeReadDto { public int SeriesId { get; init; } public int VolumeId { get; init; } diff --git a/API/DTOs/Reader/MarkVolumesReadDto.cs b/API/DTOs/Reader/MarkVolumesReadDto.cs index b07bfbc67..ebe1cd76c 100644 --- a/API/DTOs/Reader/MarkVolumesReadDto.cs +++ b/API/DTOs/Reader/MarkVolumesReadDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Reader; /// /// This is used for bulk updating a set of volume and or chapters in one go /// -public sealed record MarkVolumesReadDto +public class MarkVolumesReadDto { public int SeriesId { get; set; } /// diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/API/DTOs/Reader/PersonalToCDto.cs index c979d9d78..6763a157a 100644 --- a/API/DTOs/Reader/PersonalToCDto.cs +++ b/API/DTOs/Reader/PersonalToCDto.cs @@ -1,8 +1,6 @@ namespace API.DTOs.Reader; -#nullable enable - -public sealed record PersonalToCDto +public class PersonalToCDto { public required int ChapterId { get; set; } public required int PageNumber { get; set; } diff --git a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs b/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs index ecbb744c8..ed6368a4f 100644 --- a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs +++ b/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public sealed record RemoveBookmarkForSeriesDto +public class RemoveBookmarkForSeriesDto { public int SeriesId { get; init; } } diff --git a/API/DTOs/ReadingLists/CBL/CblBook.cs b/API/DTOs/ReadingLists/CBL/CblBook.cs index d51795b8d..0bf16a1a4 100644 --- a/API/DTOs/ReadingLists/CBL/CblBook.cs +++ b/API/DTOs/ReadingLists/CBL/CblBook.cs @@ -1,11 +1,10 @@ using System.Xml.Serialization; -using API.Data.Metadata; namespace API.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Book")] -public sealed record CblBook +public class CblBook { [XmlAttribute("Series")] public string Series { get; set; } @@ -22,12 +21,6 @@ public sealed record CblBook [XmlAttribute("Year")] public string Year { get; set; } /// - /// Main Series, Annual, Limited Series - /// - /// This maps to Format tag - [XmlAttribute("Format")] - public string Format { get; set; } - /// /// The underlying filetype /// /// This is not part of the standard and explicitly for Kavita to support non cbz/cbr files diff --git a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs b/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs index 35234923f..70a002884 100644 --- a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs +++ b/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.ReadingLists.CBL; -public sealed record CblConflictQuestion +public class CblConflictQuestion { public string SeriesName { get; set; } public IList LibrariesIds { get; set; } diff --git a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs b/API/DTOs/ReadingLists/CBL/CblImportSummary.cs index b9716421e..136a31aa8 100644 --- a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs +++ b/API/DTOs/ReadingLists/CBL/CblImportSummary.cs @@ -75,7 +75,7 @@ public enum CblImportReason InvalidFile = 9, } -public sealed record CblBookResult +public class CblBookResult { /// /// Order in the CBL @@ -114,7 +114,7 @@ public sealed record CblBookResult /// /// Represents the summary from the Import of a given CBL /// -public sealed record CblImportSummaryDto +public class CblImportSummaryDto { public string CblName { get; set; } /// diff --git a/API/DTOs/ReadingLists/CBL/CblReadingList.cs b/API/DTOs/ReadingLists/CBL/CblReadingList.cs index 15b349f42..001e6434b 100644 --- a/API/DTOs/ReadingLists/CBL/CblReadingList.cs +++ b/API/DTOs/ReadingLists/CBL/CblReadingList.cs @@ -5,7 +5,7 @@ namespace API.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Books")] -public sealed record CblBooks +public class CblBooks { [XmlElement(ElementName="Book")] public List Book { get; set; } @@ -13,7 +13,7 @@ public sealed record CblBooks [XmlRoot(ElementName="ReadingList")] -public sealed record CblReadingList +public class CblReadingList { /// /// Name of the Reading List diff --git a/API/DTOs/ReadingLists/CreateReadingListDto.cs b/API/DTOs/ReadingLists/CreateReadingListDto.cs index 543215722..783253007 100644 --- a/API/DTOs/ReadingLists/CreateReadingListDto.cs +++ b/API/DTOs/ReadingLists/CreateReadingListDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public sealed record CreateReadingListDto +public class CreateReadingListDto { public string Title { get; init; } = default!; } diff --git a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs b/API/DTOs/ReadingLists/DeleteReadingListsDto.cs deleted file mode 100644 index 8ce92f939..000000000 --- a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs.ReadingLists; - -public sealed record DeleteReadingListsDto -{ - [Required] - public IList ReadingListIds { get; set; } -} diff --git a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs b/API/DTOs/ReadingLists/PromoteReadingListsDto.cs deleted file mode 100644 index 8915274de..000000000 --- a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace API.DTOs.ReadingLists; - -public sealed record PromoteReadingListsDto -{ - public IList ReadingListIds { get; init; } - public bool Promoted { get; init; } -} diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/API/DTOs/ReadingLists/ReadingListCast.cs deleted file mode 100644 index 855bb12b7..000000000 --- a/API/DTOs/ReadingLists/ReadingListCast.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.Person; - -namespace API.DTOs.ReadingLists; - -public sealed record ReadingListCast -{ - public ICollection Writers { get; set; } = []; - public ICollection CoverArtists { get; set; } = []; - public ICollection Publishers { get; set; } = []; - public ICollection Characters { get; set; } = []; - public ICollection Pencillers { get; set; } = []; - public ICollection Inkers { get; set; } = []; - public ICollection Imprints { get; set; } = []; - public ICollection Colorists { get; set; } = []; - public ICollection Letterers { get; set; } = []; - public ICollection Editors { get; set; } = []; - public ICollection Translators { get; set; } = []; - public ICollection Teams { get; set; } = []; - public ICollection Locations { get; set; } = []; -} diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs index 47a526411..f8791b0d6 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/API/DTOs/ReadingLists/ReadingListDto.cs @@ -1,11 +1,8 @@ using System; -using API.Entities.Enums; -using API.Entities.Interfaces; namespace API.DTOs.ReadingLists; -#nullable enable -public sealed record ReadingListDto : IHasCoverImage +public class ReadingListDto { public int Id { get; init; } public string Title { get; set; } = default!; @@ -18,16 +15,7 @@ public sealed record ReadingListDto : IHasCoverImage /// /// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set. /// - public string? CoverImage { get; set; } = string.Empty; - - public string? PrimaryColor { get; set; } = string.Empty; - public string? SecondaryColor { get; set; } = string.Empty; - - /// - /// Number of Items in the Reading List - /// - public int ItemCount { get; set; } - + public string CoverImage { get; set; } = string.Empty; /// /// Minimum Year the Reading List starts /// @@ -44,20 +32,5 @@ public sealed record ReadingListDto : IHasCoverImage /// Maximum Month the Reading List starts /// public int EndingMonth { get; set; } - /// - /// The highest age rating from all Series within the reading list - /// - public required AgeRating AgeRating { get; set; } = AgeRating.Unknown; - - /// - /// Username of the User that owns (in the case of a promoted list) - /// - public string OwnerUserName { get; set; } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } } diff --git a/API/DTOs/ReadingLists/ReadingListInfoDto.cs b/API/DTOs/ReadingLists/ReadingListInfoDto.cs deleted file mode 100644 index 64a305f43..000000000 --- a/API/DTOs/ReadingLists/ReadingListInfoDto.cs +++ /dev/null @@ -1,26 +0,0 @@ -using API.DTOs.Reader; -using API.Entities.Interfaces; - -namespace API.DTOs.ReadingLists; - -public sealed record ReadingListInfoDto : IHasReadTimeEstimate -{ - /// - /// Total Pages across all Reading List Items - /// - public int Pages { get; set; } - /// - /// Total Word count across all Reading List Items - /// - public long WordCount { get; set; } - /// - /// Are ALL Reading List Items epub - /// - public bool IsAllEpub { get; set; } - /// - public int MinHoursToRead { get; set; } - /// - public int MaxHoursToRead { get; set; } - /// - public float AvgHoursToRead { get; set; } -} diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/API/DTOs/ReadingLists/ReadingListItemDto.cs index 8edec14f1..6d35a2961 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/API/DTOs/ReadingLists/ReadingListItemDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.ReadingLists; #nullable enable -public sealed record ReadingListItemDto +public class ReadingListItemDto { public int Id { get; init; } public int Order { get; init; } @@ -25,7 +25,7 @@ public sealed record ReadingListItemDto /// /// Release Date from Chapter /// - public DateTime? ReleaseDate { get; set; } + public DateTime ReleaseDate { get; set; } /// /// Used internally only /// @@ -33,16 +33,10 @@ public sealed record ReadingListItemDto /// /// The last time a reading list item (underlying chapter) was read by current authenticated user /// - public DateTime? LastReadingProgressUtc { get; set; } + public DateTime LastReadingProgressUtc { get; set; } /// /// File size of underlying item /// /// This is only used for CDisplayEx public long FileSize { get; set; } - /// - /// The chapter summary - /// - public string? Summary { get; set; } - - public bool IsSpecial { get; set; } } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs index 6624c8a5c..985f86ac0 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListByChapterDto +public class UpdateReadingListByChapterDto { public int ChapterId { get; init; } public int SeriesId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs index ba7625088..408963529 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListByMultipleDto +public class UpdateReadingListByMultipleDto { public int SeriesId { get; init; } public int ReadingListId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs index 910a5744d..f910e9c06 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListByMultipleSeriesDto +public class UpdateReadingListByMultipleSeriesDto { public int ReadingListId { get; init; } public IReadOnlyList SeriesIds { get; init; } = default!; diff --git a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs b/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs index 4bb4aa7bb..0590882bd 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListBySeriesDto +public class UpdateReadingListBySeriesDto { public int SeriesId { get; init; } public int ReadingListId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs index 422d1cc34..f77c7d63a 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListByVolumeDto +public class UpdateReadingListByVolumeDto { public int VolumeId { get; init; } public int SeriesId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/API/DTOs/ReadingLists/UpdateReadingListDto.cs index de273d825..6b590707a 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public sealed record UpdateReadingListDto +public class UpdateReadingListDto { [Required] public int ReadingListId { get; set; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs b/API/DTOs/ReadingLists/UpdateReadingListPosition.cs index 04f2501a8..3d0487144 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListPosition.cs @@ -5,7 +5,7 @@ namespace API.DTOs.ReadingLists; /// /// DTO for moving a reading list item to another position within the same list /// -public sealed record UpdateReadingListPosition +public class UpdateReadingListPosition { [Required] public int ReadingListId { get; set; } [Required] public int ReadingListItemId { get; set; } diff --git a/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs b/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs new file mode 100644 index 000000000..b01f1369c --- /dev/null +++ b/API/DTOs/Recommendation/ExternalSeriesDetailDto.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using API.DTOs.Scrobbling; + +namespace API.DTOs.Recommendation; + +public class ExternalSeriesDetailDto +{ + public string Name { get; set; } + public int? AniListId { get; set; } + public long? MALId { get; set; } + public IList Synonyms { get; set; } + public MediaFormat PlusMediaFormat { get; set; } + public string? SiteUrl { get; set; } + public string? CoverUrl { get; set; } + public IList Genres { get; set; } + public IList Staff { get; set; } + public IList Tags { get; set; } + public string? Summary { get; set; } + public int? VolumeCount { get; set; } + public int? ChapterCount { get; set; } +} diff --git a/API/DTOs/Recommendation/ExternalSeriesDto.cs b/API/DTOs/Recommendation/ExternalSeriesDto.cs index 752001a39..f3ceb1b27 100644 --- a/API/DTOs/Recommendation/ExternalSeriesDto.cs +++ b/API/DTOs/Recommendation/ExternalSeriesDto.cs @@ -1,9 +1,7 @@ -using API.Services.Plus; - -namespace API.DTOs.Recommendation; +namespace API.DTOs.Recommendation; #nullable enable -public sealed record ExternalSeriesDto +public class ExternalSeriesDto { public required string Name { get; set; } public required string CoverUrl { get; set; } @@ -11,7 +9,4 @@ public sealed record ExternalSeriesDto public string? Summary { get; set; } public int? AniListId { get; set; } public long? MalId { get; set; } - public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList; - - } diff --git a/API/DTOs/Recommendation/MetadataTagDto.cs b/API/DTOs/Recommendation/MetadataTagDto.cs index a7eb76284..b219dedc1 100644 --- a/API/DTOs/Recommendation/MetadataTagDto.cs +++ b/API/DTOs/Recommendation/MetadataTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Recommendation; -public sealed record MetadataTagDto +public class MetadataTagDto { public string Name { get; set; } public string Description { get; private set; } diff --git a/API/DTOs/Recommendation/RecommendationDto.cs b/API/DTOs/Recommendation/RecommendationDto.cs index 387661324..679245a87 100644 --- a/API/DTOs/Recommendation/RecommendationDto.cs +++ b/API/DTOs/Recommendation/RecommendationDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Recommendation; -public sealed record RecommendationDto +public class RecommendationDto { public IList OwnedSeries { get; set; } = new List(); public IList ExternalSeries { get; set; } = new List(); diff --git a/API/DTOs/Recommendation/SeriesStaffDto.cs b/API/DTOs/Recommendation/SeriesStaffDto.cs index e074e8625..ae57249f2 100644 --- a/API/DTOs/Recommendation/SeriesStaffDto.cs +++ b/API/DTOs/Recommendation/SeriesStaffDto.cs @@ -1,11 +1,8 @@ namespace API.DTOs.Recommendation; -#nullable enable -public sealed record SeriesStaffDto +public class SeriesStaffDto { public required string Name { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } public required string Url { get; set; } public required string Role { get; set; } public string? ImageUrl { get; set; } diff --git a/API/DTOs/RefreshSeriesDto.cs b/API/DTOs/RefreshSeriesDto.cs index ad26afba2..64a684394 100644 --- a/API/DTOs/RefreshSeriesDto.cs +++ b/API/DTOs/RefreshSeriesDto.cs @@ -3,7 +3,7 @@ /// /// Used for running some task against a Series. /// -public sealed record RefreshSeriesDto +public class RefreshSeriesDto { /// /// Library Id series belongs to @@ -18,9 +18,4 @@ public sealed record RefreshSeriesDto /// /// This is expensive if true. Defaults to true. public bool ForceUpdate { get; init; } = true; - /// - /// Should the task force re-calculation of colorscape. - /// - /// This is expensive if true. Defaults to true. - public bool ForceColorscape { get; init; } = false; } diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index e117af872..b6132046f 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -1,17 +1,16 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs; -#nullable enable -public sealed record RegisterDto +public class RegisterDto { [Required] public string Username { get; init; } = default!; /// /// An email to register with. Optional. Provides Forgot Password functionality /// - public string? Email { get; set; } = default!; + public string Email { get; init; } = default!; [Required] - [StringLength(256, MinimumLength = 6)] + [StringLength(32, MinimumLength = 6)] public string Password { get; set; } = default!; } diff --git a/API/DTOs/ScanFolderDto.cs b/API/DTOs/ScanFolderDto.cs index 141f7f0b5..684de909e 100644 --- a/API/DTOs/ScanFolderDto.cs +++ b/API/DTOs/ScanFolderDto.cs @@ -3,7 +3,7 @@ /// /// DTO for requesting a folder to be scanned /// -public sealed record ScanFolderDto +public class ScanFolderDto { /// /// Api key for a user with Admin permissions diff --git a/API/DTOs/Scrobbling/MalUserInfoDto.cs b/API/DTOs/Scrobbling/MalUserInfoDto.cs deleted file mode 100644 index b6fefc053..000000000 --- a/API/DTOs/Scrobbling/MalUserInfoDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs.Scrobbling; - -/// -/// Information about a User's MAL connection -/// -public sealed record MalUserInfoDto -{ - public required string Username { get; set; } - /// - /// This is actually the Client Id - /// - public required string AccessToken { get; set; } -} diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/API/DTOs/Scrobbling/MediaRecommendationDto.cs deleted file mode 100644 index 476d77279..000000000 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using API.Services.Plus; - -namespace API.DTOs.Scrobbling; -#nullable enable - -public sealed record MediaRecommendationDto -{ - public int Rating { get; set; } - public IEnumerable RecommendationNames { get; set; } = null!; - public string Name { get; set; } - public string CoverUrl { get; set; } - public string SiteUrl { get; set; } - public string? Summary { get; set; } - public int? AniListId { get; set; } - public long? MalId { get; set; } - public ScrobbleProvider Provider { get; set; } -} diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs deleted file mode 100644 index 4d0ef4ea1..000000000 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace API.DTOs.Scrobbling; -#nullable enable - -/// -/// Represents information about a potential Series for Kavita+ -/// -public sealed record PlusSeriesRequestDto -{ - public int? AniListId { get; set; } - public long? MalId { get; set; } - public string? GoogleBooksId { get; set; } - public string? MangaDexId { get; set; } - /// - /// ComicBookRoundup Id - /// - public int? CbrId { get; set; } - public string SeriesName { get; set; } - public string? AltSeriesName { get; set; } - public PlusMediaFormat MediaFormat { get; set; } - /// - /// Optional but can help with matching - /// - public int? ChapterCount { get; set; } - /// - /// Optional but can help with matching - /// - public int? VolumeCount { get; set; } - public int? Year { get; set; } -} diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/API/DTOs/Scrobbling/ScrobbleDto.cs index b90441059..ca2c2e528 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleDto.cs @@ -22,7 +22,7 @@ public enum ScrobbleEventType /// /// Represents PlusMediaFormat /// -public enum PlusMediaFormat +public enum MediaFormat { [Description("Manga")] Manga = 1, @@ -36,7 +36,7 @@ public enum PlusMediaFormat } -public sealed record ScrobbleDto +public class ScrobbleDto { /// /// User's access token to allow us to talk on their behalf @@ -44,7 +44,7 @@ public sealed record ScrobbleDto public string AniListToken { get; set; } public string SeriesName { get; set; } public string LocalizedSeriesName { get; set; } - public PlusMediaFormat Format { get; set; } + public MediaFormat Format { get; set; } public int? Year { get; set; } /// /// Optional AniListId if present on Kavita's WebLinks diff --git a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs b/API/DTOs/Scrobbling/ScrobbleErrorDto.cs index 7caaad1ca..da85f28f1 100644 --- a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleErrorDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Scrobbling; -public sealed record ScrobbleErrorDto +public class ScrobbleErrorDto { /// /// Developer defined string diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 562d923ff..25690da82 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -1,22 +1,18 @@ using System; namespace API.DTOs.Scrobbling; -#nullable enable -public sealed record ScrobbleEventDto +public class ScrobbleEventDto { - public long Id { get; init; } public string SeriesName { get; set; } public int SeriesId { get; set; } public int LibraryId { get; set; } public bool IsProcessed { get; set; } - public float? VolumeNumber { get; set; } + public int? VolumeNumber { get; set; } public int? ChapterNumber { get; set; } public DateTime LastModifiedUtc { get; set; } public DateTime CreatedUtc { get; set; } public float? Rating { get; set; } public ScrobbleEventType ScrobbleEventType { get; set; } - public bool IsErrored { get; set; } - public string? ErrorDetails { get; set; } } diff --git a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs b/API/DTOs/Scrobbling/ScrobbleHoldDto.cs index 3e09e4799..dcfe7726f 100644 --- a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleHoldDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Scrobbling; -public sealed record ScrobbleHoldDto +public class ScrobbleHoldDto { public string SeriesName { get; set; } public int SeriesId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs index ad66729d0..f714d1eda 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -1,13 +1,11 @@ namespace API.DTOs.Scrobbling; -#nullable enable /// /// Response from Kavita+ Scrobble API /// -public sealed record ScrobbleResponseDto +public class ScrobbleResponseDto { public bool Successful { get; set; } public string? ErrorMessage { get; set; } - public string? ExtraInformation {get; set;} public int RateLeft { get; set; } } diff --git a/API/DTOs/Search/BookmarkSearchResultDto.cs b/API/DTOs/Search/BookmarkSearchResultDto.cs deleted file mode 100644 index c11d2a2b8..000000000 --- a/API/DTOs/Search/BookmarkSearchResultDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace API.DTOs.Search; - -public sealed record BookmarkSearchResultDto -{ - public int LibraryId { get; set; } - public int VolumeId { get; set; } - public int SeriesId { get; set; } - public int ChapterId { get; set; } - public string SeriesName { get; set; } - public string LocalizedSeriesName { get; set; } -} diff --git a/API/DTOs/Search/SearchResultDto.cs b/API/DTOs/Search/SearchResultDto.cs index c497b55dd..6fcae3b5d 100644 --- a/API/DTOs/Search/SearchResultDto.cs +++ b/API/DTOs/Search/SearchResultDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Search; -public sealed record SearchResultDto +public class SearchResultDto { public int SeriesId { get; init; } public string Name { get; init; } = default!; diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index 11c4bdc08..66370fb0a 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -1,9 +1,6 @@ using System.Collections.Generic; -using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Reader; using API.DTOs.ReadingLists; namespace API.DTOs.Search; @@ -11,18 +8,17 @@ namespace API.DTOs.Search; /// /// Represents all Search results for a query /// -public sealed record SearchResultGroupDto +public class SearchResultGroupDto { public IEnumerable Libraries { get; set; } = default!; public IEnumerable Series { get; set; } = default!; - public IEnumerable Collections { get; set; } = default!; + public IEnumerable Collections { get; set; } = default!; public IEnumerable ReadingLists { get; set; } = default!; public IEnumerable Persons { get; set; } = default!; public IEnumerable Genres { get; set; } = default!; public IEnumerable Tags { get; set; } = default!; public IEnumerable Files { get; set; } = default!; public IEnumerable Chapters { get; set; } = default!; - public IEnumerable Bookmarks { get; set; } = default!; } diff --git a/API/DTOs/SeriesByIdsDto.cs b/API/DTOs/SeriesByIdsDto.cs index cb4c52b1e..12e13d96f 100644 --- a/API/DTOs/SeriesByIdsDto.cs +++ b/API/DTOs/SeriesByIdsDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs; -public sealed record SeriesByIdsDto +public class SeriesByIdsDto { public int[] SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs index 1bea81c84..df4cc1a07 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -2,10 +2,10 @@ namespace API.DTOs.SeriesDetail; -public sealed record NextExpectedChapterDto +public class NextExpectedChapterDto { public float ChapterNumber { get; set; } - public float VolumeNumber { get; set; } + public int VolumeNumber { get; set; } /// /// Null if not applicable /// diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs index a186dc295..72271ff73 100644 --- a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; -public sealed record RelatedSeriesDto +public class RelatedSeriesDto { /// /// The parent relationship Series @@ -22,5 +22,4 @@ public sealed record RelatedSeriesDto public IEnumerable Doujinshis { get; set; } = default!; public IEnumerable Parent { get; set; } = default!; public IEnumerable Editions { get; set; } = default!; - public IEnumerable Annuals { get; set; } = default!; } diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/API/DTOs/SeriesDetail/SeriesDetailDto.cs index c4f15552d..65d657c67 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.SeriesDetail; /// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout. /// This is subject to change, do not rely on this Data model. /// -public sealed record SeriesDetailDto +public class SeriesDetailDto { /// /// Specials for the Series. These will have their title and range cleaned to remove the special marker and prepare diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs deleted file mode 100644 index 95f5f39bd..000000000 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; - -namespace API.DTOs.SeriesDetail; -#nullable enable - -/// -/// All the data from Kavita+ for Series Detail -/// -/// This is what the UI sees, not what the API sends back -public sealed record SeriesDetailPlusDto -{ - public RecommendationDto? Recommendations { get; set; } - public IEnumerable Reviews { get; set; } - public IEnumerable? Ratings { get; set; } - public ExternalSeriesDetailDto? Series { get; set; } -} diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs index a1bb2057e..8a81f766e 100644 --- a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; -public sealed record UpdateRelatedSeriesDto +public class UpdateRelatedSeriesDto { public int SeriesId { get; set; } public IList Adaptations { get; set; } = default!; @@ -17,5 +17,4 @@ public sealed record UpdateRelatedSeriesDto public IList AlternativeVersions { get; set; } = default!; public IList Doujinshis { get; set; } = default!; public IList Editions { get; set; } = default!; - public IList Annuals { get; set; } = default!; } diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs index 7af9441c1..550969787 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -1,10 +1,11 @@ - -namespace API.DTOs.SeriesDetail; -#nullable enable +using System.ComponentModel.DataAnnotations; -public sealed record UpdateUserReviewDto +namespace API.DTOs.SeriesDetail; + +public class UpdateUserReviewDto { public int SeriesId { get; set; } - public int? ChapterId { get; set; } + [MaxLength(120)] + public string? Tagline { get; set; } public string Body { get; set; } } diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/API/DTOs/SeriesDetail/UserReviewDto.cs index 9e05bbd65..4f74dadbb 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UserReviewDto.cs @@ -1,45 +1,38 @@ -using API.Entities; -using API.Entities.Enums; -using API.Services.Plus; +using API.Services.Plus; namespace API.DTOs.SeriesDetail; -#nullable enable /// /// Represents a User Review for a given Series /// /// The user does not need to be a Kavita user -public sealed record UserReviewDto +public class UserReviewDto { /// /// A tagline for the review /// - /// This is not possible to set as a local user public string? Tagline { get; set; } + /// /// The main review /// public string Body { get; set; } - /// - /// The main body with just text, for review preview - /// - public string? BodyJustText { get; set; } + /// /// The series this is for /// public int SeriesId { get; set; } - public int? ChapterId { get; set; } + /// /// The library this series belongs in /// public int LibraryId { get; set; } + /// /// The user who wrote this /// public string Username { get; set; } - public int TotalVotes { get; set; } - public float Rating { get; set; } - public string? RawBody { get; set; } + /// /// How many upvotes this review has gotten /// @@ -48,17 +41,18 @@ public sealed record UserReviewDto /// /// If External, the url of the review /// - public string? SiteUrl { get; set; } + public string? ExternalUrl { get; set; } /// /// Does this review come from an external Source /// public bool IsExternal { get; set; } + /// + /// The main body with just text, for review preview + /// + public string? BodyJustText { get; set; } + /// /// If this review is External, which Provider did it come from /// public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita; - /// - /// Source of the Rating - /// - public RatingAuthority Authority { get; set; } = RatingAuthority.User; } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 8a49d4c05..a8ec37d9c 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -5,21 +5,14 @@ using API.Entities.Interfaces; namespace API.DTOs; #nullable enable -public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage +public class SeriesDto : IHasReadTimeEstimate { - /// public int Id { get; init; } - /// public string? Name { get; init; } - /// public string? OriginalName { get; init; } - /// public string? LocalizedName { get; init; } - /// public string? SortName { get; init; } - /// public int Pages { get; init; } - /// public bool CoverImageLocked { get; set; } /// /// Sum of pages read from linked Volumes. Calculated at API-time. @@ -29,7 +22,9 @@ public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage /// DateTime representing last time the series was Read. Calculated at API-time. /// public DateTime LatestReadDate { get; set; } - /// + /// + /// DateTime representing last time a chapter was added to the Series + /// public DateTime LastChapterAdded { get; set; } /// /// Rating from logged in user. Calculated at API-time. @@ -40,19 +35,17 @@ public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage /// public bool HasUserRated { get; set; } - /// public MangaFormat Format { get; set; } - /// public DateTime Created { get; set; } - /// + public bool NameLocked { get; set; } public bool SortNameLocked { get; set; } - /// public bool LocalizedNameLocked { get; set; } - /// + /// + /// Total number of words for the series. Only applies to epubs. + /// public long WordCount { get; set; } - /// public int LibraryId { get; set; } public string LibraryName { get; set; } = default!; /// @@ -60,30 +53,13 @@ public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage /// public int MaxHoursToRead { get; set; } /// - public float AvgHoursToRead { get; set; } - /// + public int AvgHoursToRead { get; set; } + /// + /// The highest level folder for this Series + /// public string FolderPath { get; set; } = default!; - /// - public string? LowestFolderPath { get; set; } - /// + /// + /// The last time the folder for this series was scanned + /// public DateTime LastFolderScanned { get; set; } - #region KavitaPlus - /// - public bool DontMatch { get; set; } - /// - public bool IsBlacklisted { get; set; } - #endregion - - /// - public string? CoverImage { get; set; } - /// - public string? PrimaryColor { get; set; } = string.Empty; - /// - public string? SecondaryColor { get; set; } = string.Empty; - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } } diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs index fa745148e..8230134be 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/API/DTOs/SeriesMetadataDto.cs @@ -1,15 +1,20 @@ using System.Collections.Generic; +using API.DTOs.CollectionTags; using API.DTOs.Metadata; -using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs; -public sealed record SeriesMetadataDto +public class SeriesMetadataDto { public int Id { get; set; } public string Summary { get; set; } = string.Empty; + /// + /// Collections the Series belongs to + /// + public ICollection CollectionTags { get; set; } = new List(); + /// /// Genres for the Series /// @@ -25,14 +30,10 @@ public sealed record SeriesMetadataDto public ICollection Characters { get; set; } = new List(); public ICollection Pencillers { get; set; } = new List(); public ICollection Inkers { get; set; } = new List(); - public ICollection Imprints { get; set; } = new List(); public ICollection Colorists { get; set; } = new List(); public ICollection Letterers { get; set; } = new List(); public ICollection Editors { get; set; } = new List(); public ICollection Translators { get; set; } = new List(); - public ICollection Teams { get; set; } = new List(); - public ICollection Locations { get; set; } = new List(); - /// /// Highest Age Rating from all Chapters /// @@ -74,19 +75,16 @@ public sealed record SeriesMetadataDto public bool PublicationStatusLocked { get; set; } public bool GenresLocked { get; set; } public bool TagsLocked { get; set; } - public bool WriterLocked { get; set; } - public bool CharacterLocked { get; set; } - public bool ColoristLocked { get; set; } - public bool EditorLocked { get; set; } - public bool InkerLocked { get; set; } - public bool ImprintLocked { get; set; } - public bool LettererLocked { get; set; } - public bool PencillerLocked { get; set; } - public bool PublisherLocked { get; set; } - public bool TranslatorLocked { get; set; } - public bool TeamLocked { get; set; } - public bool LocationLocked { get; set; } - public bool CoverArtistLocked { get; set; } + public bool WritersLocked { get; set; } + public bool CharactersLocked { get; set; } + public bool ColoristsLocked { get; set; } + public bool EditorsLocked { get; set; } + public bool InkersLocked { get; set; } + public bool LetterersLocked { get; set; } + public bool PencillersLocked { get; set; } + public bool PublishersLocked { get; set; } + public bool TranslatorsLocked { get; set; } + public bool CoverArtistsLocked { get; set; } public bool ReleaseYearLocked { get; set; } diff --git a/API/DTOs/Settings/SMTPConfigDto.cs b/API/DTOs/Settings/SMTPConfigDto.cs deleted file mode 100644 index c14140062..000000000 --- a/API/DTOs/Settings/SMTPConfigDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace API.DTOs.Settings; - -public sealed record SmtpConfigDto -{ - public string SenderAddress { get; set; } = string.Empty; - public string SenderDisplayName { get; set; } = string.Empty; - public string UserName { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public string Host { get; set; } = string.Empty; - public int Port { get; set; } = 0; - public bool EnableSsl { get; set; } = true; - /// - /// Limit in bytes for allowing files to be added as attachments. Defaults to 25MB - /// - public int SizeLimit { get; set; } = 26_214_400; - /// - /// Should Kavita use config/templates for Email templates or the default ones - /// - public bool CustomizedTemplates { get; set; } = false; -} diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 372042250..e405758bc 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -1,22 +1,18 @@ -using System; -using System.Text.Json.Serialization; -using API.Entities.Enums; +using API.Entities.Enums; using API.Services; namespace API.DTOs.Settings; -#nullable enable -public sealed record ServerSettingDto +public class ServerSettingDto { public string CacheDirectory { get; set; } = default!; public string TaskScan { get; set; } = default!; - public string TaskBackup { get; set; } = default!; - public string TaskCleanup { get; set; } = default!; /// /// Logging level for server. Managed in appsettings.json. /// public string LoggingLevel { get; set; } = default!; + public string TaskBackup { get; set; } = default!; /// /// Port the server listens on. Managed in appsettings.json. /// @@ -42,11 +38,15 @@ public sealed record ServerSettingDto /// /// If null or empty string, will default back to default install setting aka public string BookmarksDirectory { get; set; } = default!; + /// + /// Email service to use for the invite user flow, forgot password, etc. + /// + /// If null or empty string, will default back to default install setting aka + public string EmailServiceUrl { get; set; } = default!; public string InstallVersion { get; set; } = default!; /// /// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs. /// - public string InstallId { get; set; } = default!; /// /// The format that should be used when saving media for Kavita @@ -88,37 +88,4 @@ public sealed record ServerSettingDto /// How large the cover images should be /// public CoverImageSize CoverImageSize { get; set; } - /// - /// SMTP Configuration - /// - public SmtpConfigDto SmtpConfig { get; set; } - /// - /// The Date Kavita was first installed - /// - public DateTime? FirstInstallDate { get; set; } - /// - /// The Version of Kavita on the first run - /// - public string? FirstInstallVersion { get; set; } - - /// - /// Are at least some basics filled in - /// - /// - public bool IsEmailSetup() - { - return !string.IsNullOrEmpty(SmtpConfig.Host) - && !string.IsNullOrEmpty(SmtpConfig.SenderAddress) - && !string.IsNullOrEmpty(HostName); - } - - /// - /// Are at least some basics filled in, but not hostname as not required for Send to Device - /// - /// - public bool IsEmailSetupForSendToDevice() - { - return !string.IsNullOrEmpty(SmtpConfig.Host) - && !string.IsNullOrEmpty(SmtpConfig.SenderAddress); - } } diff --git a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs b/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs index ae1d927a9..1b081913d 100644 --- a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs +++ b/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SideNav; -public sealed record BulkUpdateSideNavStreamVisibilityDto +public class BulkUpdateSideNavStreamVisibilityDto { public required IList Ids { get; set; } public required bool Visibility { get; set; } diff --git a/API/DTOs/SideNav/ExternalSourceDto.cs b/API/DTOs/SideNav/ExternalSourceDto.cs index 382124e8a..e9ae03066 100644 --- a/API/DTOs/SideNav/ExternalSourceDto.cs +++ b/API/DTOs/SideNav/ExternalSourceDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SideNav; -public sealed record ExternalSourceDto +public class ExternalSourceDto { public required int Id { get; set; } = 0; public required string Name { get; set; } diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/API/DTOs/SideNav/SideNavStreamDto.cs index f4c196244..1f3453611 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/API/DTOs/SideNav/SideNavStreamDto.cs @@ -2,9 +2,8 @@ using API.Entities.Enums; namespace API.DTOs.SideNav; -#nullable enable -public sealed record SideNavStreamDto +public class SideNavStreamDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/StandaloneChapterDto.cs b/API/DTOs/StandaloneChapterDto.cs deleted file mode 100644 index 2f4cd2ee1..000000000 --- a/API/DTOs/StandaloneChapterDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using API.Entities.Enums; - -namespace API.DTOs; -#nullable enable - -/// -/// Used on Person Profile page -/// -public class StandaloneChapterDto : ChapterDto -{ - public int SeriesId { get; set; } - public int LibraryId { get; set; } - public LibraryType LibraryType { get; set; } - public string VolumeTitle { get; set; } -} diff --git a/API/DTOs/Statistics/Count.cs b/API/DTOs/Statistics/Count.cs index 1577e682c..411b44897 100644 --- a/API/DTOs/Statistics/Count.cs +++ b/API/DTOs/Statistics/Count.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Statistics; -public sealed record StatCount : ICount +public class StatCount : ICount { public T Value { get; set; } = default!; public long Count { get; set; } diff --git a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs index 7a248caef..c0d65fe7f 100644 --- a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs +++ b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs @@ -2,9 +2,8 @@ using API.Entities.Enums; namespace API.DTOs.Statistics; -#nullable enable -public sealed record FileExtensionDto +public class FileExtensionDto { public string? Extension { get; set; } public MangaFormat Format { get; set; } @@ -12,7 +11,7 @@ public sealed record FileExtensionDto public long TotalFiles { get; set; } } -public sealed record FileExtensionBreakdownDto +public class FileExtensionBreakdownDto { /// /// Total bytes for all files diff --git a/API/DTOs/Statistics/PagesReadOnADayCount.cs b/API/DTOs/Statistics/PagesReadOnADayCount.cs index fc56d9cc0..b1a6bb1ea 100644 --- a/API/DTOs/Statistics/PagesReadOnADayCount.cs +++ b/API/DTOs/Statistics/PagesReadOnADayCount.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Statistics; -public sealed record PagesReadOnADayCount : ICount +public class PagesReadOnADayCount : ICount { /// /// The day of the readings diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/API/DTOs/Statistics/ReadHistoryEvent.cs index 5d8262aef..9e32aa792 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/API/DTOs/Statistics/ReadHistoryEvent.cs @@ -1,12 +1,11 @@ using System; namespace API.DTOs.Statistics; -#nullable enable /// /// Represents a single User's reading event /// -public sealed record ReadHistoryEvent +public class ReadHistoryEvent { public int UserId { get; set; } public required string? UserName { get; set; } = default!; @@ -14,7 +13,6 @@ public sealed record ReadHistoryEvent public int SeriesId { get; set; } public required string SeriesName { get; set; } = default!; public DateTime ReadDate { get; set; } - public DateTime ReadDateUtc { get; set; } public int ChapterId { get; set; } - public required float ChapterNumber { get; set; } = default!; + public required string ChapterNumber { get; set; } = default!; } diff --git a/API/DTOs/Statistics/ServerStatisticsDto.cs b/API/DTOs/Statistics/ServerStatisticsDto.cs index 3d22d9a56..57fd5abce 100644 --- a/API/DTOs/Statistics/ServerStatisticsDto.cs +++ b/API/DTOs/Statistics/ServerStatisticsDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Statistics; #nullable enable -public sealed record ServerStatisticsDto +public class ServerStatisticsDto { public long ChapterCount { get; set; } public long VolumeCount { get; set; } diff --git a/API/DTOs/Statistics/TopReadsDto.cs b/API/DTOs/Statistics/TopReadsDto.cs index d11594dca..819e55ad5 100644 --- a/API/DTOs/Statistics/TopReadsDto.cs +++ b/API/DTOs/Statistics/TopReadsDto.cs @@ -1,18 +1,17 @@ namespace API.DTOs.Statistics; -#nullable enable -public sealed record TopReadDto +public class TopReadDto { public int UserId { get; set; } public string? Username { get; set; } = default!; /// /// Amount of time read on Comic libraries /// - public float ComicsTime { get; set; } + public long ComicsTime { get; set; } /// /// Amount of time read on /// - public float BooksTime { get; set; } - public float MangaTime { get; set; } + public long BooksTime { get; set; } + public long MangaTime { get; set; } } diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/API/DTOs/Statistics/UserReadStatistics.cs index 5c6935c6e..5e3f5aa5d 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/API/DTOs/Statistics/UserReadStatistics.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; namespace API.DTOs.Statistics; -#nullable enable -public sealed record UserReadStatistics +public class UserReadStatistics { /// /// Total number of pages read diff --git a/API/DTOs/Stats/FileExtensionExportDto.cs b/API/DTOs/Stats/FileExtensionExportDto.cs deleted file mode 100644 index e881960a5..000000000 --- a/API/DTOs/Stats/FileExtensionExportDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using CsvHelper.Configuration.Attributes; - -namespace API.DTOs.Stats; - -/// -/// Excel export for File Extension Report -/// -public sealed record FileExtensionExportDto -{ - [Name("Path")] - public string FilePath { get; set; } - - [Name("Extension")] - public string Extension { get; set; } -} diff --git a/API/DTOs/Stats/FileFormatDto.cs b/API/DTOs/Stats/FileFormatDto.cs new file mode 100644 index 000000000..6319bd2a9 --- /dev/null +++ b/API/DTOs/Stats/FileFormatDto.cs @@ -0,0 +1,15 @@ +using API.Entities.Enums; + +namespace API.DTOs.Stats; + +public class FileFormatDto +{ + /// + /// The extension with the ., in lowercase + /// + public required string Extension { get; set; } + /// + /// Format of extension + /// + public required MangaFormat Format { get; set; } +} diff --git a/API/DTOs/Stats/ServerInfoDto.cs b/API/DTOs/Stats/ServerInfoDto.cs new file mode 100644 index 000000000..b7b9ef526 --- /dev/null +++ b/API/DTOs/Stats/ServerInfoDto.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using API.Entities.Enums; + +namespace API.DTOs.Stats; + +/// +/// Represents information about a Kavita Installation +/// +public class ServerInfoDto +{ + /// + /// Unique Id that represents a unique install + /// + public required string InstallId { get; set; } + public required string Os { get; set; } + /// + /// If the Kavita install is using Docker + /// + public bool IsDocker { get; set; } + /// + /// Version of .NET instance is running + /// + public required string DotnetVersion { get; set; } + /// + /// Version of Kavita + /// + public required string KavitaVersion { get; set; } + /// + /// Number of Cores on the instance + /// + public int NumOfCores { get; set; } + /// + /// The number of libraries on the instance + /// + public int NumberOfLibraries { get; set; } + /// + /// Does any user have bookmarks + /// + public bool HasBookmarks { get; set; } + /// + /// The site theme the install is using + /// + /// Introduced in v0.5.2 + public string? ActiveSiteTheme { get; set; } + /// + /// The reading mode the main user has as a preference + /// + /// Introduced in v0.5.2 + public ReaderMode MangaReaderMode { get; set; } + + /// + /// Number of users on the install + /// + /// Introduced in v0.5.2 + public int NumberOfUsers { get; set; } + + /// + /// Number of collections on the install + /// + /// Introduced in v0.5.2 + public int NumberOfCollections { get; set; } + /// + /// Number of reading lists on the install (Sum of all users) + /// + /// Introduced in v0.5.2 + public int NumberOfReadingLists { get; set; } + /// + /// Is OPDS enabled + /// + /// Introduced in v0.5.2 + public bool OPDSEnabled { get; set; } + /// + /// Total number of files in the instance + /// + /// Introduced in v0.5.2 + public int TotalFiles { get; set; } + /// + /// Total number of Genres in the instance + /// + /// Introduced in v0.5.4 + public int TotalGenres { get; set; } + /// + /// Total number of People in the instance + /// + /// Introduced in v0.5.4 + public int TotalPeople { get; set; } + /// + /// Number of users on this instance using Card Layout + /// + /// Introduced in v0.5.4 + public int UsersOnCardLayout { get; set; } + /// + /// Number of users on this instance using List Layout + /// + /// Introduced in v0.5.4 + public int UsersOnListLayout { get; set; } + /// + /// Max number of Series for any library on the instance + /// + /// Introduced in v0.5.4 + public int MaxSeriesInALibrary { get; set; } + /// + /// Max number of Volumes for any library on the instance + /// + /// Introduced in v0.5.4 + public int MaxVolumesInASeries { get; set; } + /// + /// Max number of Chapters for any library on the instance + /// + /// Introduced in v0.5.4 + public int MaxChaptersInASeries { get; set; } + /// + /// Does this instance have relationships setup between series + /// + /// Introduced in v0.5.4 + public bool UsingSeriesRelationships { get; set; } + /// + /// A list of background colors set on the instance + /// + /// Introduced in v0.6.0 + public required IEnumerable MangaReaderBackgroundColors { get; set; } + /// + /// A list of Page Split defaults being used on the instance + /// + /// Introduced in v0.6.0 + public required IEnumerable MangaReaderPageSplittingModes { get; set; } + /// + /// A list of Layout Mode defaults being used on the instance + /// + /// Introduced in v0.6.0 + public required IEnumerable MangaReaderLayoutModes { get; set; } + /// + /// A list of file formats existing in the instance + /// + /// Introduced in v0.6.0 + public required IEnumerable FileFormats { get; set; } + /// + /// If there is at least one user that is using an age restricted profile on the instance + /// + /// Introduced in v0.6.0 + public bool UsingRestrictedProfiles { get; set; } + /// + /// Number of users using the Emulate Comic Book setting + /// + /// Introduced in v0.7.0 + public int UsersWithEmulateComicBook { get; set; } + /// + /// Percent (0.0-1.0) of libraries with folder watching enabled + /// + /// Introduced in v0.7.0 + public float PercentOfLibrariesWithFolderWatchingEnabled { get; set; } + /// + /// Percent (0.0-1.0) of libraries included in Search + /// + /// Introduced in v0.7.0 + public float PercentOfLibrariesIncludedInSearch { get; set; } + /// + /// Percent (0.0-1.0) of libraries included in Recommended + /// + /// Introduced in v0.7.0 + public float PercentOfLibrariesIncludedInRecommended { get; set; } + /// + /// Percent (0.0-1.0) of libraries included in Dashboard + /// + /// Introduced in v0.7.0 + public float PercentOfLibrariesIncludedInDashboard { get; set; } + /// + /// Total reading hours of all users + /// + /// Introduced in v0.7.0 + public long TotalReadingHours { get; set; } + /// + /// The encoding the server is using to save media + /// + /// Added in v0.7.3 + public EncodeFormat EncodeMediaAs { get; set; } + /// + /// The last user reading progress on the server (in UTC) + /// + /// Added in v0.7.4 + public DateTime LastReadTime { get; set; } +} diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/API/DTOs/Stats/ServerInfoSlimDto.cs index f1abb2e1d..e8db6a2b0 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/API/DTOs/Stats/ServerInfoSlimDto.cs @@ -1,12 +1,9 @@ -using System; - -namespace API.DTOs.Stats; -#nullable enable +namespace API.DTOs.Stats; /// /// This is just for the Server tab on UI /// -public sealed record ServerInfoSlimDto +public class ServerInfoSlimDto { /// /// Unique Id that represents a unique install @@ -20,13 +17,5 @@ public sealed record ServerInfoSlimDto /// Version of Kavita /// public required string KavitaVersion { get; set; } - /// - /// The Date Kavita was first installed - /// - public DateTime? FirstInstallDate { get; set; } - /// - /// The Version of Kavita on the first run - /// - public string? FirstInstallVersion { get; set; } } diff --git a/API/DTOs/Stats/V3/LibraryStatV3.cs b/API/DTOs/Stats/V3/LibraryStatV3.cs deleted file mode 100644 index 33ac86d2b..000000000 --- a/API/DTOs/Stats/V3/LibraryStatV3.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using API.Entities.Enums; - -namespace API.DTOs.Stats.V3; - -public sealed record LibraryStatV3 -{ - public bool IncludeInDashboard { get; set; } - public bool IncludeInSearch { get; set; } - public bool UsingFolderWatching { get; set; } - /// - /// Are any exclude patterns setup - /// - public bool UsingExcludePatterns { get; set; } - /// - /// Will this library create collections from ComicInfo - /// - public bool CreateCollectionsFromMetadata { get; set; } - /// - /// Will this library create reading lists from ComicInfo - /// - public bool CreateReadingListsFromMetadata { get; set; } - /// - /// Type of the Library - /// - public LibraryType LibraryType { get; set; } - public ICollection FileTypes { get; set; } - /// - /// Last time library was fully scanned - /// - public DateTime LastScanned { get; set; } - /// - /// Number of folders the library has - /// - public int NumberOfFolders { get; set; } - - -} diff --git a/API/DTOs/Stats/V3/RelationshipStatV3.cs b/API/DTOs/Stats/V3/RelationshipStatV3.cs deleted file mode 100644 index 37b63cb9a..000000000 --- a/API/DTOs/Stats/V3/RelationshipStatV3.cs +++ /dev/null @@ -1,12 +0,0 @@ -using API.Entities.Enums; - -namespace API.DTOs.Stats.V3; - -/// -/// KavitaStats - Information about Series Relationships -/// -public sealed record RelationshipStatV3 -{ - public int Count { get; set; } - public RelationKind Relationship { get; set; } -} diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs deleted file mode 100644 index 8ed3079f5..000000000 --- a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using API.Entities.Enums; - -namespace API.DTOs.Stats.V3; - -/// -/// Represents information about a Kavita Installation for Kavita Stats v3 API -/// -public sealed record ServerInfoV3Dto -{ - /// - /// Unique Id that represents a unique install - /// - public required string InstallId { get; set; } - public required string Os { get; set; } - /// - /// If the Kavita install is using Docker - /// - public bool IsDocker { get; set; } - /// - /// Version of .NET instance is running - /// - public required string DotnetVersion { get; set; } - /// - /// Version of Kavita - /// - public required string KavitaVersion { get; set; } - /// - /// Version of Kavita on Installation - /// - public required string InitialKavitaVersion { get; set; } - /// - /// Date of first Installation - /// - public DateTime InitialInstallDate { get; set; } - /// - /// Number of Cores on the instance - /// - public int NumOfCores { get; set; } - /// - /// OS locale on the instance - /// - public string OsLocale { get; set; } - /// - /// Milliseconds to open a random archive (zip/cbz) for reading - /// - public long TimeToOpeCbzMs { get; set; } - /// - /// Number of pages for said archive (zip/cbz) - /// - public long TimeToOpenCbzPages { get; set; } - /// - /// Milliseconds to get a response from KavitaStats API - /// - /// This pings a health check and does not capture any IP Information - public long TimeToPingKavitaStatsApi { get; set; } - /// - /// If using the downloading metadata feature - /// - /// Kavita+ Only - public bool MatchedMetadataEnabled { get; set; } - - - - #region Media - /// - /// Number of collections on the install - /// - public int NumberOfCollections { get; set; } - /// - /// Number of reading lists on the install (Sum of all users) - /// - public int NumberOfReadingLists { get; set; } - /// - /// Total number of files in the instance - /// - public int TotalFiles { get; set; } - /// - /// Total number of Genres in the instance - /// - public int TotalGenres { get; set; } - /// - /// Total number of Series in the instance - /// - public int TotalSeries { get; set; } - /// - /// Total number of Libraries in the instance - /// - public int TotalLibraries { get; set; } - /// - /// Total number of People in the instance - /// - public int TotalPeople { get; set; } - /// - /// Max number of Series for any library on the instance - /// - public int MaxSeriesInALibrary { get; set; } - /// - /// Max number of Volumes for any library on the instance - /// - public int MaxVolumesInASeries { get; set; } - /// - /// Max number of Chapters for any library on the instance - /// - public int MaxChaptersInASeries { get; set; } - /// - /// Everything about the Libraries on the instance - /// - public IList Libraries { get; set; } - /// - /// Everything around Series Relationships between series - /// - public IList Relationships { get; set; } - #endregion - - #region Server - /// - /// Is OPDS enabled - /// - public bool OpdsEnabled { get; set; } - /// - /// The encoding the server is using to save media - /// - public EncodeFormat EncodeMediaAs { get; set; } - /// - /// The last user reading progress on the server (in UTC) - /// - public DateTime LastReadTime { get; set; } - /// - /// Is this server using Kavita+ - /// - public bool ActiveKavitaPlusSubscription { get; set; } - #endregion - - #region Users - /// - /// If there is at least one user that is using an age restricted profile on the instance - /// - /// Introduced in v0.6.0 - public bool UsingRestrictedProfiles { get; set; } - - public IList Users { get; set; } - - #endregion -} diff --git a/API/DTOs/Stats/V3/UserStatV3.cs b/API/DTOs/Stats/V3/UserStatV3.cs deleted file mode 100644 index 450a2e409..000000000 --- a/API/DTOs/Stats/V3/UserStatV3.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using API.Data.Misc; -using API.Entities.Enums.Device; - -namespace API.DTOs.Stats.V3; - -public sealed record UserStatV3 -{ - public AgeRestriction AgeRestriction { get; set; } - /// - /// The last reading progress on the server (in UTC) - /// - public DateTime LastReadTime { get; set; } - /// - /// The last login on the server (in UTC) - /// - public DateTime LastLogin { get; set; } - /// - /// Has the user gone through email confirmation - /// - public bool IsEmailConfirmed { get; set; } - /// - /// Is the Email a valid address - /// - public bool HasValidEmail { get; set; } - /// - /// Float between 0-1 to showcase how much of the libraries a user has access to - /// - public float PercentageOfLibrariesHasAccess { get; set; } - /// - /// Number of reading lists this user created - /// - public int ReadingListsCreatedCount { get; set; } - /// - /// Number of collections this user created - /// - public int CollectionsCreatedCount { get; set; } - /// - /// Number of series in want to read for this user - /// - public int WantToReadSeriesCount { get; set; } - /// - /// Active locale for the user - /// - public string Locale { get; set; } - /// - /// Active Theme (name) - /// - public string ActiveTheme { get; set; } - /// - /// Number of series with Bookmarks created - /// - public int SeriesBookmarksCreatedCount { get; set; } - /// - /// Kavita+ only - Has an AniList Token set - /// - public bool HasAniListToken { get; set; } - /// - /// Kavita+ only - Has a MAL Token set - /// - public bool HasMALToken { get; set; } - /// - /// Number of Smart Filters a user has created - /// - public int SmartFilterCreatedCount { get; set; } - /// - /// Is the user sharing reviews - /// - public bool IsSharingReviews { get; set; } - /// - /// The number of devices setup and their platforms - /// - public ICollection DevicePlatforms { get; set; } - /// - /// Roles for this user - /// - public ICollection Roles { get; set; } - - -} diff --git a/API/DTOs/System/DirectoryDto.cs b/API/DTOs/System/DirectoryDto.cs index 3b1408f7f..e6e94f4e4 100644 --- a/API/DTOs/System/DirectoryDto.cs +++ b/API/DTOs/System/DirectoryDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.System; -public sealed record DirectoryDto +public class DirectoryDto { /// /// Name of the directory diff --git a/API/DTOs/TachiyomiChapterDto.cs b/API/DTOs/TachiyomiChapterDto.cs deleted file mode 100644 index ecdd5115c..000000000 --- a/API/DTOs/TachiyomiChapterDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs; -#nullable enable - -/// -/// This is explicitly for Tachiyomi. Number field was removed in v0.8.0, but Tachiyomi needs it for the hacks. -/// -public class TachiyomiChapterDto : ChapterDto -{ - /// - /// Smallest number of the Range. - /// - public string Number { get; init; } = default!; -} diff --git a/API/DTOs/Theme/ColorScapeDto.cs b/API/DTOs/Theme/ColorScapeDto.cs deleted file mode 100644 index 2ebd96e2b..000000000 --- a/API/DTOs/Theme/ColorScapeDto.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace API.DTOs.Theme; -#nullable enable - -/// -/// A set of colors for the color scape system in the UI -/// -public sealed record ColorScapeDto -{ - public string? Primary { get; set; } - public string? Secondary { get; set; } - - public ColorScapeDto(string? primary, string? secondary) - { - Primary = primary; - Secondary = secondary; - } - - public static readonly ColorScapeDto Empty = new ColorScapeDto(null, null); -} diff --git a/API/DTOs/Theme/DownloadableSiteThemeDto.cs b/API/DTOs/Theme/DownloadableSiteThemeDto.cs deleted file mode 100644 index b27263d92..000000000 --- a/API/DTOs/Theme/DownloadableSiteThemeDto.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace API.DTOs.Theme; - - -public sealed record DownloadableSiteThemeDto -{ - /// - /// Theme Name - /// - public string Name { get; set; } - /// - /// Url to download css file - /// - public string CssUrl { get; set; } - public string CssFile { get; set; } - /// - /// Url to preview image - /// - public IList PreviewUrls { get; set; } - /// - /// If Already downloaded - /// - public bool AlreadyDownloaded { get; set; } - /// - /// Sha of the file - /// - public string Sha { get; set; } - /// - /// Path of the Folder the files reside in - /// - public string Path { get; set; } - /// - /// Author of the theme - /// - /// Derived from Readme - public string Author { get; set; } - /// - /// Last version tested against - /// - /// Derived from Readme - public string LastCompatibleVersion { get; set; } - /// - /// If version compatible with version - /// - public bool IsCompatible { get; set; } - /// - /// Small blurb about the Theme - /// - public string Description { get; set; } -} diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/API/DTOs/Theme/SiteThemeDto.cs index 7ae8369e9..18a281b56 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/API/DTOs/Theme/SiteThemeDto.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using API.Entities.Enums.Theme; using API.Services; @@ -7,7 +6,7 @@ namespace API.DTOs.Theme; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui /// -public sealed record SiteThemeDto +public class SiteThemeDto { public int Id { get; set; } /// @@ -31,21 +30,5 @@ public sealed record SiteThemeDto /// Where did the theme come from /// public ThemeProvider Provider { get; set; } - - public IList PreviewUrls { get; set; } - /// - /// Information about the theme - /// - public string Description { get; set; } - /// - /// Author of the Theme (only applies to non-system provided themes) - /// - public string Author { get; set; } - /// - /// Last compatible version. System provided will always be most current - /// - public string CompatibleVersion { get; set; } - - public string Selector => "bg-" + Name.ToLower(); } diff --git a/API/DTOs/Theme/UpdateDefaultThemeDto.cs b/API/DTOs/Theme/UpdateDefaultThemeDto.cs index aac0858c3..0f2b129f3 100644 --- a/API/DTOs/Theme/UpdateDefaultThemeDto.cs +++ b/API/DTOs/Theme/UpdateDefaultThemeDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Theme; -public sealed record UpdateDefaultThemeDto +public class UpdateDefaultThemeDto { public int ThemeId { get; set; } } diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/API/DTOs/Update/UpdateNotificationDto.cs index b535684f0..95719bb27 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/API/DTOs/Update/UpdateNotificationDto.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices.JavaScript; - -namespace API.DTOs.Update; +namespace API.DTOs.Update; /// /// Update Notification denoting a new release available for user to update to /// -public sealed record UpdateNotificationDto +public class UpdateNotificationDto { /// /// Current installed Version @@ -16,7 +13,7 @@ public sealed record UpdateNotificationDto /// Semver of the release version /// 0.4.3 /// - public required string UpdateVersion { get; set; } + public required string UpdateVersion { get; init; } /// /// Release body in HTML /// @@ -24,11 +21,11 @@ public sealed record UpdateNotificationDto /// /// Title of the release /// - public required string UpdateTitle { get; set; } + public required string UpdateTitle { get; init; } /// /// Github Url /// - public required string UpdateUrl { get; set; } + public required string UpdateUrl { get; init; } /// /// If this install is within Docker /// @@ -40,31 +37,5 @@ public sealed record UpdateNotificationDto /// /// Date of the publish /// - public required string PublishDate { get; set; } - /// - /// Is the server on a nightly within this release - /// - public bool IsOnNightlyInRelease { get; set; } - /// - /// Is the server on an older version - /// - public bool IsReleaseNewer { get; set; } - /// - /// Is the server on this version - /// - public bool IsReleaseEqual { get; set; } - - public IList Added { get; set; } - public IList Removed { get; set; } - public IList Changed { get; set; } - public IList Fixed { get; set; } - public IList Theme { get; set; } - public IList Developer { get; set; } - public IList Api { get; set; } - public IList FeatureRequests { get; set; } - public IList KnownIssues { get; set; } - /// - /// The part above the changelog part - /// - public string BlogPart { get; set; } + public required string PublishDate { get; init; } } diff --git a/API/DTOs/UpdateChapterDto.cs b/API/DTOs/UpdateChapterDto.cs deleted file mode 100644 index 9ead8adc8..000000000 --- a/API/DTOs/UpdateChapterDto.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; - -namespace API.DTOs; - -public sealed record UpdateChapterDto -{ - public int Id { get; init; } - public string Summary { get; set; } = string.Empty; - - /// - /// Genres for the Chapter - /// - public ICollection Genres { get; set; } = new List(); - /// - /// Collection of all Tags from underlying chapters for a Chapter - /// - public ICollection Tags { get; set; } = new List(); - - public ICollection Writers { get; set; } = new List(); - public ICollection CoverArtists { get; set; } = new List(); - public ICollection Publishers { get; set; } = new List(); - public ICollection Characters { get; set; } = new List(); - public ICollection Pencillers { get; set; } = new List(); - public ICollection Inkers { get; set; } = new List(); - public ICollection Imprints { get; set; } = new List(); - public ICollection Colorists { get; set; } = new List(); - public ICollection Letterers { get; set; } = new List(); - public ICollection Editors { get; set; } = new List(); - public ICollection Translators { get; set; } = new List(); - public ICollection Teams { get; set; } = new List(); - public ICollection Locations { get; set; } = new List(); - - /// - /// Highest Age Rating from all Chapters - /// - public AgeRating AgeRating { get; set; } = AgeRating.Unknown; - /// - /// Language of the content (BCP-47 code) - /// - public string Language { get; set; } = string.Empty; - - - /// - /// Locked by user so metadata updates from scan loop will not override AgeRating - /// - public bool AgeRatingLocked { get; set; } - public bool TitleNameLocked { get; set; } - public bool GenresLocked { get; set; } - public bool TagsLocked { get; set; } - public bool WriterLocked { get; set; } - public bool CharacterLocked { get; set; } - public bool ColoristLocked { get; set; } - public bool EditorLocked { get; set; } - public bool InkerLocked { get; set; } - public bool ImprintLocked { get; set; } - public bool LettererLocked { get; set; } - public bool PencillerLocked { get; set; } - public bool PublisherLocked { get; set; } - public bool TranslatorLocked { get; set; } - public bool TeamLocked { get; set; } - public bool LocationLocked { get; set; } - public bool CoverArtistLocked { get; set; } - public bool LanguageLocked { get; set; } - public bool SummaryLocked { get; set; } - public bool ISBNLocked { get; set; } - public bool ReleaseDateLocked { get; set; } - - /// - /// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden. - /// - public float SortOrder { get; set; } - /// - /// Can the sort order be updated on scan or is it locked from UI - /// - public bool SortOrderLocked { get; set; } - - /// - /// Comma-separated link of urls to external services that have some relation to the Chapter - /// - public string WebLinks { get; set; } = string.Empty; - public string ISBN { get; set; } = string.Empty; - /// - /// Date which chapter was released - /// - public DateTime ReleaseDate { get; set; } - /// - /// Chapter title - /// - /// This should not be confused with Title which is used for special filenames. - public string TitleName { get; set; } = string.Empty; -} diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index d7f314208..b7eabf52b 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs; -public sealed record UpdateLibraryDto +public class UpdateLibraryDto { [Required] public int Id { get; init; } @@ -19,6 +19,8 @@ public sealed record UpdateLibraryDto [Required] public bool IncludeInDashboard { get; init; } [Required] + public bool IncludeInRecommended { get; init; } + [Required] public bool IncludeInSearch { get; init; } [Required] public bool ManageCollections { get; init; } @@ -26,20 +28,4 @@ public sealed record UpdateLibraryDto public bool ManageReadingLists { get; init; } [Required] public bool AllowScrobbling { get; init; } - [Required] - public bool AllowMetadataMatching { get; init; } - [Required] - public bool EnableMetadata { get; init; } - [Required] - public bool RemovePrefixForSortName { get; init; } - /// - /// What types of files to allow the scanner to pickup - /// - [Required] - public ICollection FileGroupTypes { get; init; } - /// - /// A set of Glob patterns that the scanner will exclude processing - /// - [Required] - public ICollection ExcludePatterns { get; init; } } diff --git a/API/DTOs/UpdateLibraryForUserDto.cs b/API/DTOs/UpdateLibraryForUserDto.cs index 4ce8d0df8..c90b697e2 100644 --- a/API/DTOs/UpdateLibraryForUserDto.cs +++ b/API/DTOs/UpdateLibraryForUserDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public sealed record UpdateLibraryForUserDto +public class UpdateLibraryForUserDto { public required string Username { get; init; } public required IEnumerable SelectedLibraries { get; init; } = new List(); diff --git a/API/DTOs/UpdateRBSDto.cs b/API/DTOs/UpdateRBSDto.cs index fa8bb78f9..6fdce251c 100644 --- a/API/DTOs/UpdateRBSDto.cs +++ b/API/DTOs/UpdateRBSDto.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; namespace API.DTOs; -#nullable enable -public sealed record UpdateRbsDto +public class UpdateRbsDto { public required string Username { get; init; } public IList? Roles { get; init; } diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs index a4a9baf8c..52826f9d1 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/API/DTOs/UpdateSeriesDto.cs @@ -1,7 +1,6 @@ namespace API.DTOs; -#nullable enable -public sealed record UpdateSeriesDto +public class UpdateSeriesDto { public int Id { get; init; } public string? LocalizedName { get; init; } diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/API/DTOs/UpdateSeriesMetadataDto.cs index 5225f5486..cdd6c7502 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/API/DTOs/UpdateSeriesMetadataDto.cs @@ -1,6 +1,10 @@ -namespace API.DTOs; +using System.Collections.Generic; +using API.DTOs.CollectionTags; -public sealed record UpdateSeriesMetadataDto +namespace API.DTOs; + +public class UpdateSeriesMetadataDto { - public SeriesMetadataDto SeriesMetadata { get; set; } = null!; + public SeriesMetadataDto SeriesMetadata { get; set; } = default!; + public ICollection CollectionTags { get; set; } = default!; } diff --git a/API/DTOs/UpdateRatingDto.cs b/API/DTOs/UpdateSeriesRatingDto.cs similarity index 58% rename from API/DTOs/UpdateRatingDto.cs rename to API/DTOs/UpdateSeriesRatingDto.cs index 472a94fe9..5dafa35af 100644 --- a/API/DTOs/UpdateRatingDto.cs +++ b/API/DTOs/UpdateSeriesRatingDto.cs @@ -1,8 +1,7 @@ namespace API.DTOs; -public sealed record UpdateRatingDto +public class UpdateSeriesRatingDto { public int SeriesId { get; init; } - public int? ChapterId { get; init; } public float UserRating { get; init; } } diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/API/DTOs/Uploads/UploadFileDto.cs index 8d5cdf4cb..236a554b8 100644 --- a/API/DTOs/Uploads/UploadFileDto.cs +++ b/API/DTOs/Uploads/UploadFileDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Uploads; -public sealed record UploadFileDto +public class UploadFileDto { /// /// Id of the Entity @@ -10,9 +10,4 @@ public sealed record UploadFileDto /// Base Url encoding of the file to upload from (can be null) /// public required string Url { get; set; } - - /// - /// Lock the cover or not - /// - public bool LockCover { get; set; } = true; } diff --git a/API/DTOs/Uploads/UploadUrlDto.cs b/API/DTOs/Uploads/UploadUrlDto.cs index 3f4e625c3..f2699befd 100644 --- a/API/DTOs/Uploads/UploadUrlDto.cs +++ b/API/DTOs/Uploads/UploadUrlDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Uploads; -public sealed record UploadUrlDto +public class UploadUrlDto { /// /// External url diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index 88dc97a5d..f63a021f1 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -1,11 +1,9 @@  -using System; using API.DTOs.Account; namespace API.DTOs; -#nullable enable -public sealed record UserDto +public class UserDto { public string Username { get; init; } = null!; public string Email { get; init; } = null!; diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 46f42306e..41160e362 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -1,44 +1,155 @@ using System.ComponentModel.DataAnnotations; -using API.DTOs.Theme; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; namespace API.DTOs; -#nullable enable -public sealed record UserPreferencesDto +public class UserPreferencesDto { + /// + /// Manga Reader Option: What direction should the next/prev page buttons go + /// + [Required] + public ReadingDirection ReadingDirection { get; set; } + /// + /// Manga Reader Option: How should the image be scaled to screen + /// + [Required] + public ScalingOption ScalingOption { get; set; } + /// + /// Manga Reader Option: Which side of a split image should we show first + /// + [Required] + public PageSplitOption PageSplitOption { get; set; } + /// + /// Manga Reader Option: How the manga reader should perform paging or reading of the file + /// + /// Webtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging + /// by clicking top/bottom sides of reader. + /// + /// + [Required] + public ReaderMode ReaderMode { get; set; } + /// + /// Manga Reader Option: How many pages to display in the reader at once + /// + [Required] + public LayoutMode LayoutMode { get; set; } + /// + /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages + /// + [Required] + public bool EmulateBook { get; set; } + /// + /// Manga Reader Option: Background color of the reader + /// + [Required] + public string BackgroundColor { get; set; } = "#000000"; + /// + /// Manga Reader Option: Should swiping trigger pagination + /// + [Required] + public bool SwipeToPaginate { get; set; } + /// + /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction + /// + [Required] + public bool AutoCloseMenu { get; set; } + /// + /// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change + /// + [Required] + public bool ShowScreenHints { get; set; } = true; + /// + /// Book Reader Option: Override extra Margin + /// + [Required] + public int BookReaderMargin { get; set; } + /// + /// Book Reader Option: Override line-height + /// + [Required] + public int BookReaderLineSpacing { get; set; } + /// + /// Book Reader Option: Override font size + /// + [Required] + public int BookReaderFontSize { get; set; } + /// + /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override + /// + [Required] + public string BookReaderFontFamily { get; set; } = null!; + + /// + /// Book Reader Option: Allows tapping on side of screens to paginate + /// + [Required] + public bool BookReaderTapToPaginate { get; set; } + /// + /// Book Reader Option: What direction should the next/prev page buttons go + /// + [Required] + public ReadingDirection BookReaderReadingDirection { get; set; } + + /// + /// Book Reader Option: What writing style should be used, horizontal or vertical. + /// + [Required] + public WritingStyle BookReaderWritingStyle { get; set; } /// /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark [Required] - public SiteThemeDto? Theme { get; set; } + public SiteTheme? Theme { get; set; } + [Required] public string BookReaderThemeName { get; set; } = null!; + [Required] + public BookPageLayoutMode BookReaderLayoutMode { get; set; } + /// + /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. + /// + /// Defaults to false + [Required] + public bool BookReaderImmersiveMode { get; set; } = false; + /// + /// Global Site Option: If the UI should layout items as Cards or List items + /// + /// Defaults to Cards + [Required] public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards; - /// + /// + /// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already + /// + /// Defaults to false [Required] public bool BlurUnreadSummaries { get; set; } = false; - /// + /// + /// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB. + /// [Required] public bool PromptForDownloadSize { get; set; } = true; - /// + /// + /// UI Site Global Setting: Should Kavita disable CSS transitions + /// [Required] public bool NoTransitions { get; set; } = false; - /// + /// + /// When showing series, only parent series or series with no relationships will be returned + /// [Required] public bool CollapseSeriesRelationships { get; set; } = false; - /// + /// + /// UI Site Global Setting: Should series reviews be shared with all users in the server + /// [Required] public bool ShareReviews { get; set; } = false; - /// + /// + /// UI Site Global Setting: The language locale that should be used for the user + /// [Required] public string Locale { get; set; } - - /// - public bool AniListScrobblingEnabled { get; set; } - /// - public bool WantToReadSync { get; set; } } diff --git a/API/DTOs/UserReadingProfileDto.cs b/API/DTOs/UserReadingProfileDto.cs deleted file mode 100644 index 24dbf1c34..000000000 --- a/API/DTOs/UserReadingProfileDto.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; - -namespace API.DTOs; - -public sealed record UserReadingProfileDto -{ - - public int Id { get; set; } - public int UserId { get; init; } - - public string Name { get; init; } - public ReadingProfileKind Kind { get; init; } - - #region MangaReader - - /// - [Required] - public ReadingDirection ReadingDirection { get; set; } - - /// - [Required] - public ScalingOption ScalingOption { get; set; } - - /// - [Required] - public PageSplitOption PageSplitOption { get; set; } - - /// - [Required] - public ReaderMode ReaderMode { get; set; } - - /// - [Required] - public bool AutoCloseMenu { get; set; } - - /// - [Required] - public bool ShowScreenHints { get; set; } = true; - - /// - [Required] - public bool EmulateBook { get; set; } - - /// - [Required] - public LayoutMode LayoutMode { get; set; } - - /// - [Required] - public string BackgroundColor { get; set; } = "#000000"; - - /// - [Required] - public bool SwipeToPaginate { get; set; } - - /// - [Required] - public bool AllowAutomaticWebtoonReaderDetection { get; set; } - - /// - public int? WidthOverride { get; set; } - - /// - public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; - - #endregion - - #region EpubReader - - /// - [Required] - public int BookReaderMargin { get; set; } - - /// - [Required] - public int BookReaderLineSpacing { get; set; } - - /// - [Required] - public int BookReaderFontSize { get; set; } - - /// - [Required] - public string BookReaderFontFamily { get; set; } = null!; - - /// - [Required] - public bool BookReaderTapToPaginate { get; set; } - - /// - [Required] - public ReadingDirection BookReaderReadingDirection { get; set; } - - /// - [Required] - public WritingStyle BookReaderWritingStyle { get; set; } - - /// - [Required] - public string BookReaderThemeName { get; set; } = null!; - - /// - [Required] - public BookPageLayoutMode BookReaderLayoutMode { get; set; } - - /// - [Required] - public bool BookReaderImmersiveMode { get; set; } = false; - - #endregion - - #region PdfReader - - /// - [Required] - public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - - /// - [Required] - public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - - /// - [Required] - public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - - #endregion - -} diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index fffccea59..8b1d49a7a 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -1,44 +1,32 @@ -using System; + +using System; using System.Collections.Generic; using API.Entities; using API.Entities.Interfaces; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; namespace API.DTOs; -public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage +public class VolumeDto : IHasReadTimeEstimate { - /// public int Id { get; set; } - /// - public float MinNumber { get; set; } - /// - public float MaxNumber { get; set; } - /// - public string Name { get; set; } = default!; - /// - /// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14 - /// - [Obsolete("Use MinNumber")] + /// public int Number { get; set; } + + /// + public string Name { get; set; } = default!; public int Pages { get; set; } public int PagesRead { get; set; } - /// public DateTime LastModifiedUtc { get; set; } - /// public DateTime CreatedUtc { get; set; } /// /// When chapter was created in local server time /// /// This is required for Tachiyomi Extension - /// public DateTime Created { get; set; } /// /// When chapter was last modified in local server time /// /// This is required for Tachiyomi Extension - /// public DateTime LastModified { get; set; } public int SeriesId { get; set; } public ICollection Chapters { get; set; } = new List(); @@ -47,39 +35,5 @@ public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage /// public int MaxHoursToRead { get; set; } /// - public float AvgHoursToRead { get; set; } - public long WordCount { get; set; } - - /// - /// Is this a loose leaf volume - /// - /// - public bool IsLooseLeaf() - { - return MinNumber.Is(Parser.LooseLeafVolumeNumber); - } - - /// - /// Does this volume hold only specials - /// - /// - public bool IsSpecial() - { - return MinNumber.Is(Parser.SpecialVolumeNumber); - } - - /// - public string CoverImage { get; set; } - /// - private bool CoverImageLocked { get; set; } - /// - public string? PrimaryColor { get; set; } = string.Empty; - /// - public string? SecondaryColor { get; set; } = string.Empty; - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } + public int AvgHoursToRead { get; set; } } diff --git a/API/DTOs/WantToRead/UpdateWantToReadDto.cs b/API/DTOs/WantToRead/UpdateWantToReadDto.cs index a5be26857..f1b38cea2 100644 --- a/API/DTOs/WantToRead/UpdateWantToReadDto.cs +++ b/API/DTOs/WantToRead/UpdateWantToReadDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.WantToRead; /// /// A list of Series to pass when working with Want To Read APIs /// -public sealed record UpdateWantToReadDto +public class UpdateWantToReadDto { /// /// List of Series Ids that will be Added/Removed diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 7d529b1da..c20e84d2a 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -1,17 +1,12 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; -using API.Entities.History; using API.Entities.Interfaces; using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -41,13 +36,11 @@ public sealed class DataContext : IdentityDbContext ServerSetting { get; set; } = null!; public DbSet AppUserPreferences { get; set; } = null!; public DbSet SeriesMetadata { get; set; } = null!; - [Obsolete("Use AppUserCollection")] public DbSet CollectionTag { get; set; } = null!; public DbSet AppUserBookmark { get; set; } = null!; public DbSet ReadingList { get; set; } = null!; public DbSet ReadingListItem { get; set; } = null!; public DbSet Person { get; set; } = null!; - public DbSet PersonAlias { get; set; } = null!; public DbSet Genre { get; set; } = null!; public DbSet Tag { get; set; } = null!; public DbSet SiteTheme { get; set; } = null!; @@ -65,21 +58,7 @@ public sealed class DataContext : IdentityDbContext AppUserDashboardStream { get; set; } = null!; public DbSet AppUserSideNavStream { get; set; } = null!; public DbSet AppUserExternalSource { get; set; } = null!; - public DbSet ExternalReview { get; set; } = null!; - public DbSet ExternalRating { get; set; } = null!; - public DbSet ExternalSeriesMetadata { get; set; } = null!; - public DbSet ExternalRecommendation { get; set; } = null!; - public DbSet ManualMigrationHistory { get; set; } = null!; - [Obsolete("Use IsBlacklisted field on Series")] - public DbSet SeriesBlacklist { get; set; } = null!; - public DbSet AppUserCollection { get; set; } = null!; - public DbSet ChapterPeople { get; set; } = null!; - public DbSet SeriesMetadataPeople { get; set; } = null!; - public DbSet EmailHistory { get; set; } = null!; - public DbSet MetadataSettings { get; set; } = null!; - public DbSet MetadataFieldMapping { get; set; } = null!; - public DbSet AppUserChapterRating { get; set; } = null!; - public DbSet AppUserReadingProfiles { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder builder) { @@ -129,25 +108,10 @@ public sealed class DataContext : IdentityDbContext b.Locale) .IsRequired(true) .HasDefaultValue("en"); - builder.Entity() - .Property(b => b.AniListScrobblingEnabled) - .HasDefaultValue(true); - builder.Entity() - .Property(b => b.WantToReadSync) - .HasDefaultValue(true); - builder.Entity() - .Property(b => b.AllowAutomaticWebtoonReaderDetection) - .HasDefaultValue(true); builder.Entity() .Property(b => b.AllowScrobbling) .HasDefaultValue(true); - builder.Entity() - .Property(b => b.AllowMetadataMatching) - .HasDefaultValue(true); - builder.Entity() - .Property(b => b.EnableMetadata) - .HasDefaultValue(true); builder.Entity() .Property(b => b.WebLinks) @@ -173,149 +137,17 @@ public sealed class DataContext : IdentityDbContext() .HasIndex(e => e.Visible) .IsUnique(false); - - builder.Entity() - .HasOne(em => em.Series) - .WithOne(s => s.ExternalSeriesMetadata) - .HasForeignKey(em => em.SeriesId) - .OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .Property(b => b.AgeRating) - .HasDefaultValue(AgeRating.Unknown); - - // Configure the many-to-many relationship for Movie and Person - builder.Entity() - .HasKey(cp => new { cp.ChapterId, cp.PersonId, cp.Role }); - - builder.Entity() - .HasOne(cp => cp.Chapter) - .WithMany(c => c.People) - .HasForeignKey(cp => cp.ChapterId); - - builder.Entity() - .HasOne(cp => cp.Person) - .WithMany(p => p.ChapterPeople) - .HasForeignKey(cp => cp.PersonId) - .OnDelete(DeleteBehavior.Cascade); - - - builder.Entity() - .HasKey(smp => new { smp.SeriesMetadataId, smp.PersonId, smp.Role }); - - builder.Entity() - .HasOne(smp => smp.SeriesMetadata) - .WithMany(sm => sm.People) - .HasForeignKey(smp => smp.SeriesMetadataId); - - builder.Entity() - .HasOne(smp => smp.Person) - .WithMany(p => p.SeriesMetadataPeople) - .HasForeignKey(smp => smp.PersonId) - .OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .Property(b => b.OrderWeight) - .HasDefaultValue(0); - - builder.Entity() - .Property(x => x.AgeRatingMappings) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new Dictionary() - ); - - // Ensure blacklist is stored as a JSON array - builder.Entity() - .Property(x => x.Blacklist) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); - builder.Entity() - .Property(x => x.Whitelist) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); - builder.Entity() - .Property(x => x.Overrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List() - ); - - // Configure one-to-many relationship - builder.Entity() - .HasMany(x => x.FieldMappings) - .WithOne(x => x.MetadataSettings) - .HasForeignKey(x => x.MetadataSettingsId) - .OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .Property(b => b.Enabled) - .HasDefaultValue(true); - builder.Entity() - .Property(b => b.EnableCoverImage) - .HasDefaultValue(true); - - builder.Entity() - .Property(b => b.BookThemeName) - .HasDefaultValue("Dark"); - builder.Entity() - .Property(b => b.BackgroundColor) - .HasDefaultValue("#000000"); - builder.Entity() - .Property(b => b.BookReaderWritingStyle) - .HasDefaultValue(WritingStyle.Horizontal); - builder.Entity() - .Property(b => b.AllowAutomaticWebtoonReaderDetection) - .HasDefaultValue(true); - - builder.Entity() - .Property(rp => rp.LibraryIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT"); - builder.Entity() - .Property(rp => rp.SeriesIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT"); - - builder.Entity() - .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? - new List()) - .HasColumnType("TEXT") - .HasDefaultValue(new List()); - builder.Entity() - .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT") - .HasDefaultValue(new List()); } - #nullable enable + private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e) { if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return; + entity.Created = DateTime.Now; entity.LastModified = DateTime.Now; + entity.CreatedUtc = DateTime.UtcNow; entity.LastModifiedUtc = DateTime.UtcNow; - - // This allows for mocking - if (entity.Created == DateTime.MinValue) - { - entity.Created = DateTime.Now; - entity.CreatedUtc = DateTime.UtcNow; - } } private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e) @@ -324,7 +156,6 @@ public sealed class DataContext : IdentityDbContext +/// v0.7 introduced UTC dates and GMT+1 users would sometimes have dates stored as '0000-12-31 23:00:00'. +/// This Migration will update those dates. +/// +// ReSharper disable once InconsistentNaming +public static class MigrateBrokenGMT1Dates +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + // if current version is > 0.7, then we can exit and not perform + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (Version.Parse(settings.InstallVersion) > new Version(0, 7, 0, 2)) + { + return; + } + logger.LogCritical("Running MigrateBrokenGMT1Dates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); + + #region Series + logger.LogInformation("Updating Dates on Series..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Series SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE Series SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + UPDATE Series SET LastChapterAddedUtc = '0001-01-01 00:00:00' WHERE LastChapterAddedUtc = '0000-12-31 23:00:00'; + UPDATE Series SET LastFolderScannedUtc = '0001-01-01 00:00:00' WHERE LastFolderScannedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Series...Done"); + #endregion + + #region Library + logger.LogInformation("Updating Dates on Libraries..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Library SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE Library SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Libraries...Done"); + #endregion + + #region Volume + try + { + logger.LogInformation("Updating Dates on Volumes..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Volume SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE Volume SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Volumes...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Volumes...Failed"); + } + #endregion + + #region Chapter + try + { + logger.LogInformation("Updating Dates on Chapters..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Chapter SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE Chapter SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Chapters...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Chapters...Failed"); + } + #endregion + + #region AppUserBookmark + logger.LogInformation("Updating Dates on Bookmarks..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserBookmark SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE AppUserBookmark SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Bookmarks...Done"); + #endregion + + #region AppUserProgress + logger.LogInformation("Updating Dates on Progress..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserProgresses SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE AppUserProgresses SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Progress...Done"); + #endregion + + #region Device + logger.LogInformation("Updating Dates on Device..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Device SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE Device SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + UPDATE Device SET LastUsedUtc = '0001-01-01 00:00:00' WHERE LastUsedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on Device...Done"); + #endregion + + #region MangaFile + logger.LogInformation("Updating Dates on MangaFile..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE MangaFile SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE MangaFile SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + UPDATE MangaFile SET LastFileAnalysisUtc = '0001-01-01 00:00:00' WHERE LastFileAnalysisUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on MangaFile...Done"); + #endregion + + #region ReadingList + logger.LogInformation("Updating Dates on ReadingList..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE ReadingList SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE ReadingList SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on ReadingList...Done"); + #endregion + + #region SiteTheme + logger.LogInformation("Updating Dates on SiteTheme..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE SiteTheme SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; + UPDATE SiteTheme SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; + "); + logger.LogInformation("Updating Dates on SiteTheme...Done"); + #endregion + + logger.LogInformation("MigrateBrokenGMT1Dates migration finished"); + + } +} diff --git a/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs b/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs new file mode 100644 index 000000000..74344775f --- /dev/null +++ b/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; + +namespace API.Data.ManualMigrations; + +/// +/// New role introduced in v0.5.1. Adds the role to all users. +/// +public static class MigrateChangePasswordRoles +{ + /// + /// Will not run if any users have the ChangePassword role already + /// + /// + /// + public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager) + { + var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangePasswordRole); + if (usersWithRole.Count != 0) return; + + var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); + foreach (var user in allUsers) + { + await userManager.RemoveFromRoleAsync(user, "ChangePassword"); + await userManager.AddToRoleAsync(user, PolicyConstants.ChangePasswordRole); + } + } +} diff --git a/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs b/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs new file mode 100644 index 000000000..0b22b3f23 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// New role introduced in v0.6. Adds the role to all users. +/// +public static class MigrateChangeRestrictionRoles +{ + /// + /// Will not run if any users have the role already + /// + /// + /// + /// + public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager, ILogger logger) + { + var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangeRestrictionRole); + if (usersWithRole.Count != 0) return; + + logger.LogCritical("Running MigrateChangeRestrictionRoles migration"); + + var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); + foreach (var user in allUsers) + { + await userManager.RemoveFromRoleAsync(user, PolicyConstants.ChangeRestrictionRole); + await userManager.AddToRoleAsync(user, PolicyConstants.ChangeRestrictionRole); + } + + logger.LogInformation("MigrateChangeRestrictionRoles migration complete"); + } +} diff --git a/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs b/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs new file mode 100644 index 000000000..e5e0f365b --- /dev/null +++ b/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.7.8.6 explicitly introduced DashboardStream and v0.7.8.9 changed the default seed titles to use locale strings. +/// This migration will target nightly releases and should be removed before v0.7.9 release. +/// +public static class MigrateDashboardStreamNamesToLocaleKeys +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + var allStreams = await unitOfWork.UserRepository.GetAllDashboardStreams(); + if (!allStreams.Any(s => s.Name.Equals("On Deck"))) return; + + logger.LogCritical("Running MigrateDashboardStreamNamesToLocaleKeys migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); + foreach (var stream in allStreams.Where(s => s.IsProvided)) + { + stream.Name = stream.Name switch + { + "On Deck" => "on-deck", + "Recently Updated" => "recently-updated", + "Newly Added" => "newly-added", + "More In" => "more-in-genre", + _ => stream.Name + }; + unitOfWork.UserRepository.Update(stream); + } + + await unitOfWork.CommitAsync(); + logger.LogInformation("MigrateDashboardStreamNamesToLocaleKeys migration finished"); + } +} diff --git a/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs b/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs new file mode 100644 index 000000000..a432a1362 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading.Tasks; +using API.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.7.4 introduced Scrobbling with Kavita+. By default, it is on, but Comic libraries have no scrobble providers, so disable +/// +public static class MigrateDisableScrobblingOnComicLibraries +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + if (!await dataContext.Library.Where(s => s.Type == LibraryType.Comic).Where(l => l.AllowScrobbling).AnyAsync()) + { + return; + } + logger.LogInformation("Running MigrateDisableScrobblingOnComicLibraries migration. Please be patient, this may take some time"); + + + foreach (var lib in await dataContext.Library.Where(s => s.Type == LibraryType.Comic).Where(l => l.AllowScrobbling).ToListAsync()) + { + lib.AllowScrobbling = false; + unitOfWork.LibraryRepository.Update(lib); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + + logger.LogInformation("MigrateDisableScrobblingOnComicLibraries migration finished"); + + } + +} diff --git a/API/Data/ManualMigrations/MigrateExistingRatings.cs b/API/Data/ManualMigrations/MigrateExistingRatings.cs new file mode 100644 index 000000000..4314c724b --- /dev/null +++ b/API/Data/ManualMigrations/MigrateExistingRatings.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.5.6 and v0.7.6, Ratings > 0 need to have "HasRatingSet" +/// +/// Added in v0.7.5.6 +// ReSharper disable once InconsistentNaming +public static class MigrateExistingRatings +{ + public static async Task Migrate(DataContext context, ILogger logger) + { + logger.LogCritical("Running MigrateExistingRatings migration - Please be patient, this may take some time. This is not an error"); + + foreach (var r in context.AppUserRating.Where(r => r.Rating > 0f)) + { + r.HasBeenRated = true; + context.Entry(r).State = EntityState.Modified; + } + + if (context.ChangeTracker.HasChanges()) + { + await context.SaveChangesAsync(); + } + + logger.LogCritical("Running MigrateExistingRatings migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateLoginRole.cs b/API/Data/ManualMigrations/MigrateLoginRole.cs new file mode 100644 index 000000000..0a582b761 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateLoginRole.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Added in v0.7.1.18 +/// +public static class MigrateLoginRoles +{ + /// + /// Will not run if any users have the role already + /// + /// + /// + /// + public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager, ILogger logger) + { + var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.LoginRole); + if (usersWithRole.Count != 0) return; + + logger.LogCritical("Running MigrateLoginRoles migration"); + + var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); + foreach (var user in allUsers) + { + await userManager.RemoveFromRoleAsync(user, PolicyConstants.LoginRole); + await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); + } + + logger.LogInformation("MigrateLoginRoles migration complete"); + } +} diff --git a/API/Data/ManualMigrations/MigrateNormalizedEverything.cs b/API/Data/ManualMigrations/MigrateNormalizedEverything.cs new file mode 100644 index 000000000..d5ba39ab6 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateNormalizedEverything.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated +/// +public static class MigrateNormalizedEverything +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + // if current version is > 0.5.6.5, then we can exit and not perform + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 5)) + { + return; + } + logger.LogCritical("Running MigrateNormalizedEverything migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); + + logger.LogInformation("Updating Normalization on Series..."); + foreach (var series in await dataContext.Series.ToListAsync()) + { + series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty); + series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name ?? string.Empty); + logger.LogInformation("Updated Series: {SeriesName}", series.Name); + unitOfWork.SeriesRepository.Update(series); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on Series...Done"); + + // Genres + logger.LogInformation("Updating Normalization on Genres..."); + foreach (var genre in await dataContext.Genre.ToListAsync()) + { + genre.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title ?? string.Empty); + logger.LogInformation("Updated Genre: {Genre}", genre.Title); + unitOfWork.GenreRepository.Attach(genre); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on Genres...Done"); + + // Tags + logger.LogInformation("Updating Normalization on Tags..."); + foreach (var tag in await dataContext.Tag.ToListAsync()) + { + tag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title ?? string.Empty); + logger.LogInformation("Updated Tag: {Tag}", tag.Title); + unitOfWork.TagRepository.Attach(tag); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on Tags...Done"); + + // People + logger.LogInformation("Updating Normalization on People..."); + foreach (var person in await dataContext.Person.ToListAsync()) + { + person.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name ?? string.Empty); + logger.LogInformation("Updated Person: {Person}", person.Name); + unitOfWork.PersonRepository.Attach(person); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on People...Done"); + + // Collections + logger.LogInformation("Updating Normalization on Collections..."); + foreach (var collection in await dataContext.CollectionTag.ToListAsync()) + { + collection.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(collection.Title ?? string.Empty); + logger.LogInformation("Updated Collection: {Collection}", collection.Title); + unitOfWork.CollectionTagRepository.Update(collection); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on Collections...Done"); + + // Reading Lists + logger.LogInformation("Updating Normalization on Reading Lists..."); + foreach (var readingList in await dataContext.ReadingList.ToListAsync()) + { + readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title ?? string.Empty); + logger.LogInformation("Updated Reading List: {ReadingList}", readingList.Title); + unitOfWork.ReadingListRepository.Update(readingList); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + logger.LogInformation("Updating Normalization on Reading Lists...Done"); + + + logger.LogInformation("MigrateNormalizedEverything migration finished"); + + } + +} diff --git a/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs b/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs new file mode 100644 index 000000000..dcb5e9370 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.5.6 introduced Normalized Localized Name, which allows for faster lookups and less memory usage. This migration will calculate them once +/// +public static class MigrateNormalizedLocalizedName +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + if (!await dataContext.Series.Where(s => s.NormalizedLocalizedName == null).AnyAsync()) + { + return; + } + logger.LogInformation("Running MigrateNormalizedLocalizedName migration. Please be patient, this may take some time"); + + + foreach (var series in await dataContext.Series.ToListAsync()) + { + series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty); + logger.LogInformation("Updated {SeriesName} normalized localized name: {LocalizedName}", series.Name, series.NormalizedLocalizedName); + unitOfWork.SeriesRepository.Update(series); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + + logger.LogInformation("MigrateNormalizedLocalizedName migration finished"); + + } + +} diff --git a/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs b/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs new file mode 100644 index 000000000..4541801c7 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using API.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// New role introduced in v0.6. Calculates the Age Rating on all Reading Lists +/// +public static class MigrateReadingListAgeRating +{ + /// + /// Will not run if any above v0.5.6.24 or v0.6.0 + /// + /// + /// + /// + /// + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext context, IReadingListService readingListService, ILogger logger) + { + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 26)) + { + return; + } + + logger.LogInformation("MigrateReadingListAgeRating migration starting"); + var readingLists = await context.ReadingList.Include(r => r.Items).ToListAsync(); + foreach (var readingList in readingLists) + { + await readingListService.CalculateReadingListAgeRating(readingList); + context.ReadingList.Update(readingList); + } + + await context.SaveChangesAsync(); + logger.LogInformation("MigrateReadingListAgeRating migration complete"); + } +} diff --git a/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs b/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs new file mode 100644 index 000000000..f893866bd --- /dev/null +++ b/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using API.Services.Tasks; + +namespace API.Data.ManualMigrations; + +/// +/// In v0.5.3, we removed Light and E-Ink themes. This migration will remove the themes from the DB and default anyone on +/// null, E-Ink, or Light to Dark. +/// +public static class MigrateRemoveExtraThemes +{ + public static async Task Migrate(IUnitOfWork unitOfWork, IThemeService themeService) + { + var themes = (await unitOfWork.SiteThemeRepository.GetThemes()).ToList(); + + if (themes.Find(t => t.Name.Equals("Light")) == null) + { + return; + } + + Console.WriteLine("Removing Dark and E-Ink themes"); + + var darkTheme = themes.Single(t => t.Name.Equals("Dark")); + var lightTheme = themes.Single(t => t.Name.Equals("Light")); + var eInkTheme = themes.Single(t => t.Name.Equals("E-Ink")); + + + + // Update default theme if it's not Dark or a custom theme + await themeService.UpdateDefault(darkTheme.Id); + + // Update all users to Dark theme if they are on Light/E-Ink + foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(lightTheme.Id)) + { + pref.Theme = darkTheme; + } + foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(eInkTheme.Id)) + { + pref.Theme = darkTheme; + } + + // Remove Light/E-Ink themes + foreach (var siteTheme in themes.Where(t => t.Name.Equals("Light") || t.Name.Equals("E-Ink"))) + { + unitOfWork.SiteThemeRepository.Remove(siteTheme); + } + // Commit and call it a day + await unitOfWork.CommitAsync(); + + Console.WriteLine("Completed removing Dark and E-Ink themes"); + } + +} diff --git a/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs b/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs new file mode 100644 index 000000000..bbabf1905 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using API.Entities.Enums; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Added in v0.7.2.7/v0.7.3 in which the ConvertXToWebP Setting keys were removed. This migration will remove them. +/// +public static class MigrateRemoveWebPSettingRows +{ + public static async Task Migrate(IUnitOfWork unitOfWork, ILogger logger) + { + logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Please be patient, this may take some time. This is not an error"); + + var key = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertBookmarkToWebP); + var key2 = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertCoverToWebP); + if (key == null && key2 == null) + { + logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - complete. Nothing to do"); + return; + } + + unitOfWork.SettingsRepository.Remove(key); + unitOfWork.SettingsRepository.Remove(key2); + + await unitOfWork.CommitAsync(); + + logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateToUtcDates.cs b/API/Data/ManualMigrations/MigrateToUtcDates.cs new file mode 100644 index 000000000..a1e758bdb --- /dev/null +++ b/API/Data/ManualMigrations/MigrateToUtcDates.cs @@ -0,0 +1,153 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.6.1.38 or v0.7.0, +/// +public static class MigrateToUtcDates +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + // if current version is > 0.6.1.38, then we can exit and not perform + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (Version.Parse(settings.InstallVersion) > new Version(0, 6, 1, 38)) + { + return; + } + logger.LogCritical("Running MigrateToUtcDates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); + + #region Series + logger.LogInformation("Updating Dates on Series..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Series SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastChapterAddedUtc] = datetime([LastChapterAdded], 'utc'), + [LastFolderScannedUtc] = datetime([LastFolderScanned], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Series...Done"); + #endregion + + #region Library + logger.LogInformation("Updating Dates on Libraries..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Library SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Libraries...Done"); + #endregion + + #region Volume + try + { + logger.LogInformation("Updating Dates on Volumes..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Volume SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'); + "); + logger.LogInformation("Updating Dates on Volumes...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Volumes...Failed"); + } + #endregion + + #region Chapter + try + { + logger.LogInformation("Updating Dates on Chapters..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Chapter SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Chapters...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Chapters...Failed"); + } + #endregion + + #region AppUserBookmark + logger.LogInformation("Updating Dates on Bookmarks..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserBookmark SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Bookmarks...Done"); + #endregion + + #region AppUserProgress + logger.LogInformation("Updating Dates on Progress..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserProgresses SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Progress...Done"); + #endregion + + #region Device + logger.LogInformation("Updating Dates on Device..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Device SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastUsedUtc] = datetime([LastUsed], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Device...Done"); + #endregion + + #region MangaFile + logger.LogInformation("Updating Dates on MangaFile..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE MangaFile SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastFileAnalysisUtc] = datetime([LastFileAnalysis], 'utc') + ; + "); + logger.LogInformation("Updating Dates on MangaFile...Done"); + #endregion + + #region ReadingList + logger.LogInformation("Updating Dates on ReadingList..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE ReadingList SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on ReadingList...Done"); + #endregion + + #region SiteTheme + logger.LogInformation("Updating Dates on SiteTheme..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE SiteTheme SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on SiteTheme...Done"); + #endregion + + logger.LogInformation("MigrateToUtcDates migration finished"); + + } +} diff --git a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs similarity index 88% rename from API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs rename to API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs index 5070a43d0..d4220e7f7 100644 --- a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs +++ b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs @@ -14,13 +14,9 @@ public static class MigrateUserLibrarySideNavStream { public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateUserLibrarySideNavStream")) - { - return; - } + logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error"); - var usersWithLibraryStreams = await dataContext.AppUser - .Include(u => u.SideNavStreams) + var usersWithLibraryStreams = await dataContext.AppUser.Include(u => u.SideNavStreams) .AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0)); if (usersWithLibraryStreams) @@ -29,8 +25,6 @@ public static class MigrateUserLibrarySideNavStream return; } - logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error"); - var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams); foreach (var user in users) { diff --git a/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs b/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs new file mode 100644 index 000000000..575be95ae --- /dev/null +++ b/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.6.1.8 and v0.7, this adds library ids to all User Progress to allow for easier queries against progress +/// +public static class MigrateUserProgressLibraryId +{ + public static async Task Migrate(IUnitOfWork unitOfWork, ILogger logger) + { + logger.LogCritical("Running MigrateUserProgressLibraryId migration - Please be patient, this may take some time. This is not an error"); + + var progress = await unitOfWork.AppUserProgressRepository.GetAnyProgress(); + if (progress == null || progress.LibraryId != 0) + { + logger.LogCritical("Running MigrateUserProgressLibraryId migration - complete. Nothing to do"); + return; + } + + var seriesIdsWithLibraryIds = await unitOfWork.SeriesRepository.GetLibraryIdsForSeriesAsync(); + foreach (var prog in await unitOfWork.AppUserProgressRepository.GetAllProgress()) + { + prog.LibraryId = seriesIdsWithLibraryIds[prog.SeriesId]; + unitOfWork.AppUserProgressRepository.Update(prog); + } + + + await unitOfWork.CommitAsync(); + + logger.LogCritical("Running MigrateSeriesRelationsImport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs b/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs deleted file mode 100644 index 92fbf54e6..000000000 --- a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.7.11 with the removal of .Kavitaignore files -/// -public static class MigrateLibrariesToHaveAllFileTypes -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLibrariesToHaveAllFileTypes")) - { - return; - } - - logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Please be patient, this may take some time. This is not an error"); - - var allLibs = await dataContext.Library - .Include(l => l.LibraryFileTypes) - .Where(library => library.LibraryFileTypes.Count == 0) - .ToListAsync(); - - foreach (var library in allLibs) - { - switch (library.Type) - { - case LibraryType.Manga: - case LibraryType.Comic: - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Archive - }); - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Epub - }); - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Images - }); - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Pdf - }); - break; - case LibraryType.Book: - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Pdf - }); - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Epub - }); - break; - case LibraryType.Image: - library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Images - }); - break; - default: - break; - } - } - - if (unitOfWork.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs b/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs deleted file mode 100644 index d36859e69..000000000 --- a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using API.DTOs.Filtering.v2; -using API.Entities; -using API.Entities.History; -using API.Helpers; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.7.10.2 introduced a bad encoding, this will migrate those bad smart filters -/// -public static class MigrateSmartFilterEncoding -{ - private static readonly Regex StatementsRegex = new Regex("stmts=(?.*?)&"); - private const string ValueRegex = @"value=(?\d+)"; - private const string FieldRegex = @"field=(?\d+)"; - private const string ComparisonRegex = @"comparison=(?\d+)"; - private const string SortOptionsRegex = @"sortField=(.*?),isAscending=(.*?)"; - - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateSmartFilterEncoding")) - { - return; - } - - logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error"); - - var smartFilters = await dataContext.AppUserSmartFilter.ToListAsync(); - foreach (var filter in smartFilters) - { - if (!ShouldMigrateFilter(filter.Filter)) continue; - var decode = EncodeFix(filter.Filter); - if (string.IsNullOrEmpty(decode)) continue; - filter.Filter = decode; - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateSmartFilterEncoding", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error"); - } - - public static bool ShouldMigrateFilter(string filter) - { - return !string.IsNullOrEmpty(filter) && !(filter.Contains(SmartFilterHelper.StatementSeparator) || Uri.UnescapeDataString(filter).Contains(SmartFilterHelper.StatementSeparator)); - } - - public static string EncodeFix(string encodedFilter) - { - var statements = StatementsRegex.Matches(encodedFilter) - .Select(match => match.Groups["Statements"]) - .FirstOrDefault(group => group.Success && group != Match.Empty)?.Value; - if (string.IsNullOrEmpty(statements)) return encodedFilter; - - - // We have statements. Let's remove the statements and generate a filter dto - var noStmt = StatementsRegex.Replace(encodedFilter, string.Empty).Replace("stmts=", string.Empty); - - // Pre-v0.7.10 filters could be extra escaped - if (!noStmt.Contains("sortField=")) - { - noStmt = Uri.UnescapeDataString(noStmt); - } - - // We need to replace sort options portion with a properly encoded - noStmt = Regex.Replace(noStmt, SortOptionsRegex, match => - { - var sortFieldValue = match.Groups[1].Value; - var isAscendingValue = match.Groups[2].Value; - - return $"sortField={sortFieldValue}{SmartFilterHelper.InnerStatementSeparator}isAscending={isAscendingValue}"; - }); - - //name=Zero&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1 - var filterDto = SmartFilterHelper.Decode(noStmt); - - // Now we just parse each individual stmt into the core components and add to statements - - var individualParts = Uri.UnescapeDataString(statements).Split(',').Select(Uri.UnescapeDataString); - foreach (var part in individualParts) - { - filterDto.Statements.Add(new FilterStatementDto() - { - Value = Regex.Match(part, ValueRegex).Groups["value"].Value, - Field = Enum.Parse(Regex.Match(part, FieldRegex).Groups["value"].Value), - Comparison = Enum.Parse(Regex.Match(part, ComparisonRegex).Groups["value"].Value), - }); - } - return SmartFilterHelper.Encode(filterDto); - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs b/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs deleted file mode 100644 index 89485fd71..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// For the v0.7.14 release, one of the nightlies had bad data that would cause issues. This drops those records -/// -public static class MigrateClearNightlyExternalSeriesRecords -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateClearNightlyExternalSeriesRecords")) - { - return; - } - - logger.LogCritical( - "Running MigrateClearNightlyExternalSeriesRecords migration - Please be patient, this may take some time. This is not an error"); - - dataContext.ExternalSeriesMetadata.RemoveRange(dataContext.ExternalSeriesMetadata); - dataContext.ExternalRating.RemoveRange(dataContext.ExternalRating); - dataContext.ExternalRecommendation.RemoveRange(dataContext.ExternalRecommendation); - dataContext.ExternalReview.RemoveRange(dataContext.ExternalReview); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateClearNightlyExternalSeriesRecords", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateClearNightlyExternalSeriesRecords migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs b/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs deleted file mode 100644 index 0e406c386..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Services; -using Flurl.Http; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public static class MigrateEmailTemplates -{ - private const string EmailChange = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailChange.html"; - private const string EmailConfirm = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailConfirm.html"; - private const string EmailPasswordReset = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailPasswordReset.html"; - private const string SendToDevice = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/SendToDevice.html"; - private const string EmailTest = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailTest.html"; - - public static async Task Migrate(IDirectoryService directoryService, ILogger logger) - { - var files = directoryService.GetFiles(directoryService.CustomizedTemplateDirectory); - if (files.Any()) - { - return; - } - - logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error"); - - // Write files to directory - await DownloadAndWriteToFile(EmailChange, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailChange.html"), logger); - await DownloadAndWriteToFile(EmailConfirm, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailConfirm.html"), logger); - await DownloadAndWriteToFile(EmailPasswordReset, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailPasswordReset.html"), logger); - await DownloadAndWriteToFile(SendToDevice, Path.Join(directoryService.CustomizedTemplateDirectory, "SendToDevice.html"), logger); - await DownloadAndWriteToFile(EmailTest, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailTest.html"), logger); - - - logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error"); - } - - private static async Task DownloadAndWriteToFile(string url, string filePath, ILogger logger) - { - try - { - // Download the raw text using Flurl - var content = await url.GetStringAsync(); - - // Write the content to a file - await File.WriteAllTextAsync(filePath, content); - - logger.LogInformation("{File} downloaded and written successfully", filePath); - } - catch (FlurlHttpException ex) - { - logger.LogError(ex, "Unable to download {Url} to {FilePath}. Please perform yourself!", url, filePath); - } - } - - -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs b/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs deleted file mode 100644 index eaf63c41c..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history -/// and I don't need to remove old migrations -/// -public static class MigrateManualHistory -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync()) - { - return; - } - - logger.LogCritical( - "Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error"); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateUserLibrarySideNavStream", - ProductVersion = "0.7.9.0", - RanAt = DateTime.UtcNow - }); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateSmartFilterEncoding", - ProductVersion = "0.7.11.0", - RanAt = DateTime.UtcNow - }); - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateLibrariesToHaveAllFileTypes", - ProductVersion = "0.7.11.0", - RanAt = DateTime.UtcNow - }); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateEmailTemplates", - ProductVersion = "0.7.14.0", - RanAt = DateTime.UtcNow - }); - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateVolumeNumber", - ProductVersion = "0.7.14.0", - RanAt = DateTime.UtcNow - }); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateWantToReadExport", - ProductVersion = "0.7.14.0", - RanAt = DateTime.UtcNow - }); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateWantToReadImport", - ProductVersion = "0.7.14.0", - RanAt = DateTime.UtcNow - }); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateManualHistory", - ProductVersion = "0.7.14.0", - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateManualHistory migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs b/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs deleted file mode 100644 index 38b7cfbba..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public static class MigrateVolumeLookupName -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeLookupName")) - { - return; - } - - logger.LogCritical( - "Running MigrateVolumeLookupName migration - Please be patient, this may take some time. This is not an error"); - - // Update all volumes to have LookupName as after this migration, name isn't used for lookup - var volumes = dataContext.Volume.ToList(); - foreach (var volume in volumes) - { - volume.LookupName = volume.Name; - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateVolumeLookupName", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateVolumeLookupName migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs b/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs deleted file mode 100644 index 712d826fa..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number -/// -public static class MigrateVolumeNumber -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeNumber")) - { - return; - } - - if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0)) - { - logger.LogCritical( - "Running MigrateVolumeNumber migration - Completed. This is not an error"); - return; - } - - logger.LogCritical( - "Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error"); - - // Get all volumes - foreach (var volume in dataContext.Volume) - { - volume.MinNumber = Parser.MinNumberFromRange(volume.Name); - volume.MaxNumber = Parser.MaxNumberFromRange(volume.Name); - } - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateVolumeNumber migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs b/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs deleted file mode 100644 index 95a86c370..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Threading.Tasks; -using API.Services; -using CsvHelper; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - - -/// -/// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv -/// -/// This must run BEFORE any DB migrations -public static class MigrateWantToReadExport -{ - public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger logger) - { - try - { - - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadExport")) - { - return; - } - - var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); - if (File.Exists(importFile)) - { - logger.LogCritical( - "Running MigrateWantToReadExport migration - Completed. This is not an error"); - return; - } - - logger.LogCritical( - "Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error"); - - await using var command = dataContext.Database.GetDbConnection().CreateCommand(); - command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;"; - - await dataContext.Database.OpenConnectionAsync(); - await using var result = await command.ExecuteReaderAsync(); - - await using var writer = - new StreamWriter(Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv")); - await using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture); - - // Write header - csvWriter.WriteField("AppUserId"); - csvWriter.WriteField("Id"); - await csvWriter.NextRecordAsync(); - - // Write data - while (await result.ReadAsync()) - { - var appUserId = result["AppUserId"].ToString(); - var id = result["Id"].ToString(); - - csvWriter.WriteField(appUserId); - csvWriter.WriteField(id); - await csvWriter.NextRecordAsync(); - } - - - try - { - await dataContext.Database.CloseConnectionAsync(); - writer.Close(); - } - catch (Exception) - { - /* Swallow */ - } - - logger.LogCritical( - "Running MigrateWantToReadExport migration - Completed. This is not an error"); - } - catch (Exception ex) - { - // On new installs, the db isn't setup yet, so this has nothing to do - } - } -} diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs b/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs deleted file mode 100644 index 31df056d9..000000000 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Services; -using CsvHelper; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.7.13.12/v0.7.14 - Want to read is imported from a csv -/// -public static class MigrateWantToReadImport -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, IDirectoryService directoryService, ILogger logger) - { - - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadImport")) - { - return; - } - - var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); - var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv"); - - if (!File.Exists(importFile) || File.Exists(outputFile)) - { - logger.LogCritical( - "Running MigrateWantToReadImport migration - Completed. This is not an error"); - return; - } - - logger.LogCritical( - "Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error"); - - using var reader = new StreamReader(importFile); - using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture); - // Read the records from the CSV file - await csvReader.ReadAsync(); - csvReader.ReadHeader(); // Skip the header row - - while (await csvReader.ReadAsync()) - { - // Read the values of AppUserId and Id columns - var appUserId = csvReader.GetField("AppUserId"); - var seriesId = csvReader.GetField("Id"); - var user = await unitOfWork.UserRepository.GetUserByIdAsync(appUserId, AppUserIncludes.WantToRead); - if (user == null || user.WantToRead.Any(w => w.SeriesId == seriesId)) continue; - - user.WantToRead.Add(new AppUserWantToRead() - { - SeriesId = seriesId - }); - } - - await unitOfWork.CommitAsync(); - reader.Close(); - - File.WriteAllLines(outputFile, await File.ReadAllLinesAsync(importFile)); - logger.LogCritical( - "Running MigrateWantToReadImport migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs b/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs deleted file mode 100644 index fac184dc9..000000000 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - - -/// -/// v0.8.0 migration to move loose leaf chapters into their own volume and retain user progress. -/// -public static class MigrateLooseLeafChapters -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLooseLeafChapters")) - { - return; - } - - logger.LogCritical( - "Running MigrateLooseLeafChapters migration - Please be patient, this may take some time. This is not an error"); - - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var extension = settings.EncodeMediaAs.GetExtension(); - - var progress = await dataContext.AppUserProgresses - .Join(dataContext.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new UserProgressCsvRecord - { - IsSpecial = c.IsSpecial, - AppUserId = p.AppUserId, - PagesRead = p.PagesRead, - Range = c.Range, - Number = c.Number, - MinNumber = c.MinNumber, - SeriesId = p.SeriesId, - VolumeId = p.VolumeId, - ProgressId = p.Id - }) - .Where(d => !d.IsSpecial) - .Join(dataContext.Volume, d => d.VolumeId, v => v.Id, (d, v) => new - { - ProgressRecord = d, - Volume = v - }) - .Where(d => d.Volume.Name == "0") - .ToListAsync(); - - // First, group all the progresses into different series - logger.LogCritical("Migrating {Count} progress events to new Volume structure for Loose leafs - This may take over 10 minutes depending on size of DB. Please wait", progress.Count); - var progressesGroupedBySeries = progress - .GroupBy(p => p.ProgressRecord.SeriesId); - - foreach (var seriesGroup in progressesGroupedBySeries) - { - // Get each series and move the loose leafs from the old volume to the new Volume - var seriesId = seriesGroup.Key; - - // Handle All Loose Leafs - var looseLeafsInSeries = seriesGroup - .Where(p => !p.ProgressRecord.IsSpecial) - .ToList(); - - // Get distinct Volumes by Id. For each one, create it then create the progress events - var distinctVolumes = looseLeafsInSeries.DistinctBy(d => d.Volume.Id); - foreach (var distinctVolume in distinctVolumes) - { - // Create a new volume for each series with the appropriate number (-100000) - var chapters = await dataContext.Chapter - .Where(c => c.VolumeId == distinctVolume.Volume.Id && !c.IsSpecial).ToListAsync(); - - var newVolume = new VolumeBuilder(Parser.LooseLeafVolume) - .WithSeriesId(seriesId) - .WithCreated(distinctVolume.Volume.Created) - .WithLastModified(distinctVolume.Volume.LastModified) - .Build(); - - newVolume.Pages = chapters.Sum(c => c.Pages); - newVolume.WordCount = chapters.Sum(c => c.WordCount); - newVolume.MinHoursToRead = chapters.Sum(c => c.MinHoursToRead); - newVolume.MaxHoursToRead = chapters.Sum(c => c.MaxHoursToRead); - newVolume.AvgHoursToRead = chapters.Sum(c => c.AvgHoursToRead); - dataContext.Volume.Add(newVolume); - await dataContext.SaveChangesAsync(); // Save changes to generate the newVolumeId - - // Migrate the progress event to the new volume - var oldVolumeProgresses = await dataContext.AppUserProgresses - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - foreach (var oldProgress in oldVolumeProgresses) - { - oldProgress.VolumeId = newVolume.Id; - } - - - logger.LogInformation("Moving {Count} chapters from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - chapters.Count, distinctVolume.Volume.Id, newVolume.Id); - - // Move the loose leaf chapters from the old volume to the new Volume - foreach (var chapter in chapters) - { - // Update the VolumeId on the existing progress event - chapter.VolumeId = newVolume.Id; - - // We need to migrate cover images as well - //UpdateCoverImage(directoryService, logger, chapter, extension, newVolume); - } - - - var oldVolumeBookmarks = await dataContext.AppUserBookmark - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Bookmarks from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumeBookmarks.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var bookmark in oldVolumeBookmarks) - { - bookmark.VolumeId = newVolume.Id; - } - - - var oldVolumePersonalToC = await dataContext.AppUserTableOfContent - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumePersonalToC.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var pToc in oldVolumePersonalToC) - { - pToc.VolumeId = newVolume.Id; - } - - var oldVolumeReadingListItems = await dataContext.ReadingListItem - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumeReadingListItems.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var readingListItem in oldVolumeReadingListItems) - { - readingListItem.VolumeId = newVolume.Id; - } - - - await dataContext.SaveChangesAsync(); - } - } - - // Save changes after processing all series - if (dataContext.ChangeTracker.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateLooseLeafChapters", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateLooseLeafChapters migration - Completed. This is not an error"); - } - - private static void UpdateCoverImage(IDirectoryService directoryService, ILogger logger, Chapter chapter, - string extension, Volume newVolume) - { - var existingCover = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + extension; - var newCover = ImageService.GetChapterFormat(chapter.Id, newVolume.Id) + extension; - try - { - if (!chapter.CoverImageLocked) - { - // First rename existing cover - File.Copy(Path.Join(directoryService.CoverImageDirectory, existingCover), Path.Join(directoryService.CoverImageDirectory, newCover)); - chapter.CoverImage = newCover; - } - } catch (Exception ex) - { - logger.LogError(ex, "Unable to rename {OldCover} to {NewCover}, this cover will need manual refresh", existingCover, newCover); - } - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs b/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs deleted file mode 100644 index cda83f05b..000000000 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public class UserProgressCsvRecord -{ - public bool IsSpecial { get; set; } - public int AppUserId { get; set; } - public int PagesRead { get; set; } - public string Range { get; set; } - public string Number { get; set; } - public float MinNumber { get; set; } - public int SeriesId { get; set; } - public int VolumeId { get; set; } - public int ProgressId { get; set; } -} - -/// -/// v0.8.0 migration to move Specials into their own volume and retain user progress. -/// -public static class MigrateMixedSpecials -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateMixedSpecials")) - { - return; - } - - logger.LogCritical( - "Running ManualMigrateMixedSpecials migration - Please be patient, this may take some time. This is not an error"); - - // First, group all the progresses into different series - // Get each series and move the specials from old volume to the new Volume() - // Create a new progress event from existing and store the Id of existing progress event to delete it - // Save per series - - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var extension = settings.EncodeMediaAs.GetExtension(); - - var progress = await dataContext.AppUserProgresses - .Join(dataContext.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new UserProgressCsvRecord - { - IsSpecial = c.IsSpecial, - AppUserId = p.AppUserId, - PagesRead = p.PagesRead, - Range = c.Range, - Number = c.Number, - MinNumber = c.MinNumber, - SeriesId = p.SeriesId, - VolumeId = p.VolumeId, - ProgressId = p.Id - }) - .Where(d => d.IsSpecial || d.Number == "0") - .Join(dataContext.Volume, d => d.VolumeId, v => v.Id, - (d, v) => new - { - ProgressRecord = d, - Volume = v - }) - .Where(d => d.Volume.Name == "0") - .ToListAsync(); - - // First, group all the progresses into different series - logger.LogCritical("Migrating {Count} progress events to new Volume structure for Specials - This may take over 10 minutes depending on size of DB. Please wait", progress.Count); - var progressesGroupedBySeries = progress.GroupBy(p => p.ProgressRecord.SeriesId); - - foreach (var seriesGroup in progressesGroupedBySeries) - { - // Get each series and move the specials from the old volume to the new Volume - var seriesId = seriesGroup.Key; - - // Handle All Specials - var specialsInSeries = seriesGroup - .Where(p => p.ProgressRecord.IsSpecial) - .ToList(); - - // Get distinct Volumes by Id. For each one, create it then create the progress events - var distinctVolumes = specialsInSeries.DistinctBy(d => d.Volume.Id); - foreach (var distinctVolume in distinctVolumes) - { - // Create a new volume for each series with the appropriate number (-100000) - var chapters = await dataContext.Chapter - .Where(c => c.VolumeId == distinctVolume.Volume.Id && c.IsSpecial).ToListAsync(); - - var newVolume = new VolumeBuilder(Parser.SpecialVolume) - .WithSeriesId(seriesId) - .WithCreated(distinctVolume.Volume.Created) - .WithLastModified(distinctVolume.Volume.LastModified) - .Build(); - - newVolume.Pages = chapters.Sum(c => c.Pages); - newVolume.WordCount = chapters.Sum(c => c.WordCount); - newVolume.MinHoursToRead = chapters.Sum(c => c.MinHoursToRead); - newVolume.MaxHoursToRead = chapters.Sum(c => c.MaxHoursToRead); - newVolume.AvgHoursToRead = chapters.Sum(c => c.AvgHoursToRead); - - dataContext.Volume.Add(newVolume); - await dataContext.SaveChangesAsync(); // Save changes to generate the newVolumeId - - // Migrate the progress event to the new volume - var oldVolumeProgresses = await dataContext.AppUserProgresses - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - foreach (var oldProgress in oldVolumeProgresses) - { - oldProgress.VolumeId = newVolume.Id; - } - - - logger.LogInformation("Moving {Count} chapters from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - chapters.Count, distinctVolume.Volume.Id, newVolume.Id); - - // Move the special chapters from the old volume to the new Volume - foreach (var specialChapter in chapters) - { - // Update the VolumeId on the existing progress event - specialChapter.VolumeId = newVolume.Id; - - //UpdateCoverImage(directoryService, logger, specialChapter, extension, newVolume); - } - - var oldVolumeBookmarks = await dataContext.AppUserBookmark - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Bookmarks from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumeBookmarks.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var bookmark in oldVolumeBookmarks) - { - bookmark.VolumeId = newVolume.Id; - } - - - var oldVolumePersonalToC = await dataContext.AppUserTableOfContent - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumePersonalToC.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var pToc in oldVolumePersonalToC) - { - pToc.VolumeId = newVolume.Id; - } - - var oldVolumeReadingListItems = await dataContext.ReadingListItem - .Where(p => p.VolumeId == distinctVolume.Volume.Id).ToListAsync(); - logger.LogInformation("Moving {Count} existing Personal ToC from Volume Id {OldVolumeId} to New Volume {NewVolumeId}", - oldVolumeReadingListItems.Count, distinctVolume.Volume.Id, newVolume.Id); - foreach (var readingListItem in oldVolumeReadingListItems) - { - readingListItem.VolumeId = newVolume.Id; - } - - await dataContext.SaveChangesAsync(); - } - - - } - - // Save changes after processing all series - if (dataContext.ChangeTracker.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "ManualMigrateMixedSpecials", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running ManualMigrateMixedSpecials migration - Completed. This is not an error"); - } - - private static void UpdateCoverImage(IDirectoryService directoryService, ILogger logger, Chapter specialChapter, - string extension, Volume newVolume) - { - // We need to migrate cover images as well - var existingCover = ImageService.GetChapterFormat(specialChapter.Id, specialChapter.VolumeId) + extension; - var newCover = ImageService.GetChapterFormat(specialChapter.Id, newVolume.Id) + extension; - try - { - - if (!specialChapter.CoverImageLocked) - { - // First rename existing cover - File.Copy(Path.Join(directoryService.CoverImageDirectory, existingCover), Path.Join(directoryService.CoverImageDirectory, newCover)); - specialChapter.CoverImage = newCover; - } - } catch (Exception ex) - { - logger.LogError(ex, "Unable to rename {OldCover} to {NewCover}, this cover will need manual refresh", existingCover, newCover); - } - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs b/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs deleted file mode 100644 index 7d1f2dd12..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - - - -/// -/// Introduced in v0.8.0, this migrates the existing Chapter and Volume 0 -> Parser defined, MangaFile.FileName -/// -public static class MigrateChapterFields -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterFields")) - { - return; - } - - logger.LogCritical( - "Running MigrateChapterFields migration - Please be patient, this may take some time. This is not an error"); - - // Update all volumes only have specials in them (rare) - var volumesWithJustSpecials = dataContext.Volume - .Include(v => v.Chapters) - .Where(v => v.Name == "0" && v.Chapters.All(c => c.IsSpecial)) - .ToList(); - logger.LogCritical( - "Running MigrateChapterFields migration - Updating {Count} volumes that only have specials in them", volumesWithJustSpecials.Count); - foreach (var volume in volumesWithJustSpecials) - { - volume.Name = $"{Parser.SpecialVolumeNumber}"; - volume.MinNumber = Parser.SpecialVolumeNumber; - volume.MaxNumber = Parser.SpecialVolumeNumber; - } - - // Update all volumes that only have loose leafs in them - var looseLeafVolumes = dataContext.Volume - .Include(v => v.Chapters) - .Where(v => v.Name == "0" && v.Chapters.All(c => !c.IsSpecial)) - .ToList(); - logger.LogCritical( - "Running MigrateChapterFields migration - Updating {Count} volumes that only have loose leaf chapters in them", looseLeafVolumes.Count); - foreach (var volume in looseLeafVolumes) - { - volume.Name = $"{Parser.DefaultChapterNumber}"; - volume.MinNumber = Parser.DefaultChapterNumber; - volume.MaxNumber = Parser.DefaultChapterNumber; - } - - // Update all MangaFile - logger.LogCritical( - "Running MigrateChapterFields migration - Updating all MangaFiles"); - foreach (var mangaFile in dataContext.MangaFile) - { - mangaFile.FileName = Parser.RemoveExtensionIfSupported(mangaFile.FilePath); - } - - var looseLeafChapters = await dataContext.Chapter.Where(c => c.Number == "0").ToListAsync(); - logger.LogCritical( - "Running MigrateChapterFields migration - Updating {Count} loose leaf chapters", looseLeafChapters.Count); - foreach (var chapter in looseLeafChapters) - { - chapter.Number = Parser.DefaultChapter; - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateChapterFields", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - - - logger.LogCritical( - "Running MigrateChapterFields migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs b/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs deleted file mode 100644 index e31fa4b92..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.8.0, this migrates the existing Chapter Range -> Chapter Min/Max Number -/// -public static class MigrateChapterNumber -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterNumber")) - { - return; - } - - logger.LogCritical( - "Running MigrateChapterNumber migration - Please be patient, this may take some time. This is not an error"); - - // Get all volumes - foreach (var chapter in dataContext.Chapter) - { - if (chapter.IsSpecial) - { - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; - continue; - } - chapter.MinNumber = Parser.MinNumberFromRange(chapter.Range); - chapter.MaxNumber = Parser.MaxNumberFromRange(chapter.Range); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateChapterNumber", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateChapterNumber migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs b/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs deleted file mode 100644 index 70a4b30f6..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.0 changed the range to that it doesn't have filename by default -/// -public static class MigrateChapterRange -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterRange")) - { - return; - } - - logger.LogCritical( - "Running MigrateChapterRange migration - Please be patient, this may take some time. This is not an error"); - - var chapters = await dataContext.Chapter.ToListAsync(); - foreach (var chapter in chapters) - { - if (Parser.MinNumberFromRange(chapter.Range).Is(0.0f)) - { - chapter.Range = chapter.GetNumberTitle(); - } - } - - - // Save changes after processing all series - if (dataContext.ChangeTracker.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateChapterRange", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateChapterRange migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs b/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs deleted file mode 100644 index e29e706d0..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions.QueryExtensions; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.0 refactored User Collections -/// -public static class MigrateCollectionTagToUserCollections -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateCollectionTagToUserCollections") || - !await dataContext.AppUser.AnyAsync()) - { - return; - } - - logger.LogCritical( - "Running MigrateCollectionTagToUserCollections migration - Please be patient, this may take some time. This is not an error"); - - // Find the first user that is an admin - var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections); - if (defaultAdmin == null) - { - await CompleteMigration(dataContext, logger); - return; - } - - // For all collectionTags, move them over to said user - var existingCollections = await dataContext.CollectionTag - .OrderBy(c => c.NormalizedTitle) - .Includes(CollectionTagIncludes.SeriesMetadataWithSeries) - .ToListAsync(); - foreach (var existingCollectionTag in existingCollections) - { - var collection = new AppUserCollection() - { - Title = existingCollectionTag.Title, - NormalizedTitle = existingCollectionTag.Title.Normalize(), - CoverImage = existingCollectionTag.CoverImage, - CoverImageLocked = existingCollectionTag.CoverImageLocked, - Promoted = existingCollectionTag.Promoted, - AgeRating = AgeRating.Unknown, - Summary = existingCollectionTag.Summary, - Items = existingCollectionTag.SeriesMetadatas.Select(s => s.Series).ToList() - }; - - collection.AgeRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(collection.Items.Select(s => s.Id)); - defaultAdmin.Collections.Add(collection); - } - unitOfWork.UserRepository.Update(defaultAdmin); - - await unitOfWork.CommitAsync(); - - await CompleteMigration(dataContext, logger); - } - - private static async Task CompleteMigration(DataContext dataContext, ILogger logger) - { - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateCollectionTagToUserCollections", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateCollectionTagToUserCollections migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs b/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs deleted file mode 100644 index e414cd8cc..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. -/// -public static class MigrateDuplicateDarkTheme -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateDuplicateDarkTheme")) - { - return; - } - - logger.LogCritical( - "Running MigrateDuplicateDarkTheme migration - Please be patient, this may take some time. This is not an error"); - - var darkThemes = await dataContext.SiteTheme.Where(t => t.Name == "Dark").ToListAsync(); - - if (darkThemes.Count > 1) - { - var correctDarkTheme = darkThemes.First(d => !string.IsNullOrEmpty(d.Description)); - - // Get users - var users = await dataContext.AppUser - .Include(u => u.UserPreferences) - .ThenInclude(p => p.Theme) - .Where(u => u.UserPreferences.Theme.Name == "Dark") - .ToListAsync(); - - // Find any users that have a duplicate Dark theme as default and switch to the correct one - foreach (var user in users) - { - if (string.IsNullOrEmpty(user.UserPreferences.Theme.Description)) - { - user.UserPreferences.Theme = correctDarkTheme; - } - } - await dataContext.SaveChangesAsync(); - - // Now remove the bad themes - dataContext.SiteTheme.RemoveRange(darkThemes.Where(d => string.IsNullOrEmpty(d.Description))); - - await dataContext.SaveChangesAsync(); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateDuplicateDarkTheme", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateDuplicateDarkTheme migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs b/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs deleted file mode 100644 index 1dbc7f325..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. -/// -public static class MigrateMangaFilePath -{ - public static async Task Migrate(DataContext dataContext, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateMangaFilePath")) - { - return; - } - - logger.LogCritical( - "Running MigrateMangaFilePath migration - Please be patient, this may take some time. This is not an error"); - - - foreach(var file in dataContext.MangaFile) - { - file.FilePath = Parser.NormalizePath(file.FilePath); - } - - await dataContext.SaveChangesAsync(); - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateMangaFilePath", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateMangaFilePath migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs b/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs deleted file mode 100644 index 631daeea8..000000000 --- a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services; -using CsvHelper; -using CsvHelper.Configuration.Attributes; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public class ProgressExport -{ - [Name("Library Id")] - public int LibraryId { get; set; } - - [Name("Library Name")] - public string LibraryName { get; set; } - - [Name("Series Name")] - public string SeriesName { get; set; } - - [Name("Volume Number")] - public string VolumeRange { get; set; } - - [Name("Volume LookupName")] - public string VolumeLookupName { get; set; } - - [Name("Chapter Number")] - public string ChapterRange { get; set; } - - [Name("FileName")] - public string MangaFileName { get; set; } - - [Name("FilePath")] - public string MangaFilePath { get; set; } - - [Name("AppUser Name")] - public string AppUserName { get; set; } - - [Name("AppUser Id")] - public int AppUserId { get; set; } - - [Name("Pages Read")] - public int PagesRead { get; set; } - - [Name("BookScrollId")] - public string BookScrollId { get; set; } - - [Name("Progress Created")] - public DateTime Created { get; set; } - - [Name("Progress LastModified")] - public DateTime LastModified { get; set; } -} - -/// -/// v0.8.0 - Progress is extracted and saved in a csv -/// -public static class MigrateProgressExport -{ - public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger logger) - { - try - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateProgressExport")) - { - return; - } - - logger.LogCritical( - "Running MigrateProgressExport migration - Please be patient, this may take some time. This is not an error"); - - var data = await dataContext.AppUserProgresses - .Join(dataContext.Series, progress => progress.SeriesId, series => series.Id, (progress, series) => new { progress, series }) - .Join(dataContext.Volume, ps => ps.progress.VolumeId, volume => volume.Id, (ps, volume) => new { ps.progress, ps.series, volume }) - .Join(dataContext.Chapter, psv => psv.progress.ChapterId, chapter => chapter.Id, (psv, chapter) => new { psv.progress, psv.series, psv.volume, chapter }) - .Join(dataContext.MangaFile, psvc => psvc.chapter.Id, mangaFile => mangaFile.ChapterId, (psvc, mangaFile) => new { psvc.progress, psvc.series, psvc.volume, psvc.chapter, mangaFile }) - .Join(dataContext.AppUser, psvcm => psvcm.progress.AppUserId, appUser => appUser.Id, (psvcm, appUser) => new - { - LibraryId = psvcm.series.LibraryId, - LibraryName = psvcm.series.Library.Name, - SeriesName = psvcm.series.Name, - VolumeRange = psvcm.volume.MinNumber + "-" + psvcm.volume.MaxNumber, - VolumeLookupName = psvcm.volume.Name, - ChapterRange = psvcm.chapter.Range, - MangaFileName = psvcm.mangaFile.FileName, - MangaFilePath = psvcm.mangaFile.FilePath, - AppUserName = appUser.UserName, - AppUserId = appUser.Id, - PagesRead = psvcm.progress.PagesRead, - BookScrollId = psvcm.progress.BookScrollId, - ProgressCreated = psvcm.progress.Created, - ProgressLastModified = psvcm.progress.LastModified - }).ToListAsync(); - - - // Write the mapped data to a CSV file - await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "progress_export.csv")); - await using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); - await csv.WriteRecordsAsync(data); - - logger.LogCritical( - "Running MigrateProgressExport migration - Completed. This is not an error"); - } - catch (Exception ex) - { - // On new installs, the db isn't setup yet, so this has nothing to do - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateProgressExport", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - } -} diff --git a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs b/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs deleted file mode 100644 index 2a68ca3d6..000000000 --- a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.0 released with a bug around LowestSeriesPath. This resets it for all users. -/// -public static class MigrateLowestSeriesFolderPath -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath")) - { - return; - } - - logger.LogCritical( - "Running MigrateLowestSeriesFolderPath migration - Please be patient, this may take some time. This is not an error"); - - var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync(); - foreach (var s in series) - { - s.LowestFolderPath = string.Empty; - unitOfWork.SeriesRepository.Update(s); - } - - // Save changes after processing all series - if (dataContext.ChangeTracker.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateLowestSeriesFolderPath", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateLowestSeriesFolderPath migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs b/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs deleted file mode 100644 index 21abfdf10..000000000 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.2 switches Default Kavita installs to WAL -/// -public static class ManualMigrateSwitchToWal -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateSwitchToWal")) - { - return; - } - - logger.LogCritical("Running ManualMigrateSwitchToWal migration - Please be patient, this may take some time. This is not an error"); - try - { - var connection = context.Database.GetDbConnection(); - await connection.OpenAsync(); - await using var command = connection.CreateCommand(); - command.CommandText = "PRAGMA journal_mode=WAL;"; - await command.ExecuteNonQueryAsync(); - } - catch (Exception ex) - { - logger.LogError(ex, "Error setting WAL"); - /* Swallow */ - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateSwitchToWal", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateSwitchToWal migration - Completed. This is not an error"); - } - -} diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs b/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs deleted file mode 100644 index e137afe7b..000000000 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.2 introduced Theme repo viewer, this adds Description to existing SiteTheme defaults -/// -public static class ManualMigrateThemeDescription -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateThemeDescription")) - { - return; - } - - logger.LogCritical("Running ManualMigrateThemeDescription migration - Please be patient, this may take some time. This is not an error"); - - var theme = await context.SiteTheme.FirstOrDefaultAsync(t => t.Name == "Dark"); - if (theme != null) - { - theme.Description = Seed.DefaultThemes.First().Description; - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - - - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateThemeDescription", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateThemeDescription migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs b/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs deleted file mode 100644 index 851f4ac42..000000000 --- a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Services; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.2 I started collecting information on when the user first installed Kavita as a nice to have info for the user -/// -public static class MigrateInitialInstallData -{ - public static async Task Migrate(DataContext dataContext, ILogger logger, IDirectoryService directoryService) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateInitialInstallData")) - { - return; - } - - logger.LogCritical( - "Running MigrateInitialInstallData migration - Please be patient, this may take some time. This is not an error"); - - var settings = await dataContext.ServerSetting.ToListAsync(); - - // Get the Install Date as Date the DB was written - var dbFile = Path.Join(directoryService.ConfigDirectory, "kavita.db"); - if (!string.IsNullOrEmpty(dbFile) && directoryService.FileSystem.File.Exists(dbFile)) - { - var fi = directoryService.FileSystem.FileInfo.New(dbFile); - var setting = settings.First(s => s.Key == ServerSettingKey.FirstInstallDate); - setting.Value = fi.CreationTimeUtc.ToString(CultureInfo.InvariantCulture); - await dataContext.SaveChangesAsync(); - } - - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateInitialInstallData", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical( - "Running MigrateInitialInstallData migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs b/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs deleted file mode 100644 index 8e0db3c10..000000000 --- a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; -#nullable enable - -/// -/// Some linux-based users are having non-rooted LowestFolderPaths. This will attempt to fix it or null them. -/// Fixed in v0.8.2 -/// -public static class MigrateSeriesLowestFolderPath -{ - public static async Task Migrate(DataContext dataContext, ILogger logger, IDirectoryService directoryService) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateSeriesLowestFolderPath")) - { - return; - } - - logger.LogCritical("Running MigrateSeriesLowestFolderPath migration - Please be patient, this may take some time. This is not an error"); - - var seriesWithFolderPath = - await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)) - .Include(s => s.Library) - .ThenInclude(l => l.Folders) - .ToListAsync(); - - foreach (var series in seriesWithFolderPath) - { - var isValidPath = series.Library.Folders - .Any(folder => Parser.NormalizePath(series.LowestFolderPath!).StartsWith(Parser.NormalizePath(folder.Path), StringComparison.OrdinalIgnoreCase)); - - if (isValidPath) continue; - series.LowestFolderPath = null; - dataContext.Entry(series).State = EntityState.Modified; - } - - await dataContext.SaveChangesAsync(); - - - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateSeriesLowestFolderPath", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - - logger.LogCritical("Running MigrateSeriesLowestFolderPath migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs b/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs deleted file mode 100644 index fc8b2e586..000000000 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using Flurl.Util; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// At some point, encoding settings wrote bad data to the backend, maybe in v0.8.0. This just fixes any bad data. -/// -public static class ManualMigrateEncodeSettings -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateEncodeSettings")) - { - return; - } - - logger.LogCritical("Running ManualMigrateEncodeSettings migration - Please be patient, this may take some time. This is not an error"); - - - var encodeAs = await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.EncodeMediaAs); - var coverSize = await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CoverImageSize); - - var encodeMap = new Dictionary - { - { EncodeFormat.WEBP.ToString(), ((int)EncodeFormat.WEBP).ToString() }, - { EncodeFormat.PNG.ToString(), ((int)EncodeFormat.PNG).ToString() }, - { EncodeFormat.AVIF.ToString(), ((int)EncodeFormat.AVIF).ToString() } - }; - - if (encodeMap.TryGetValue(encodeAs.Value, out var encodedValue)) - { - encodeAs.Value = encodedValue; - context.ServerSetting.Update(encodeAs); - } - - if (coverSize.Value == "0") - { - coverSize.Value = ((int)CoverImageSize.Default).ToString(); - context.ServerSetting.Update(coverSize); - } - - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateEncodeSettings", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateEncodeSettings migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs b/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs deleted file mode 100644 index 01d9ad45d..000000000 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Due to a bug in the initial merge of People/Scanner rework, people got messed up bad. This migration will clear out the table only for nightly users: 0.8.3.15/0.8.3.16 -/// -public static class ManualMigrateRemovePeople -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateRemovePeople")) - { - return; - } - - var version = BuildInfo.Version.ToString(); - if (version != "0.8.3.15" && version != "0.8.3.16") - { - return; - } - - logger.LogCritical("Running ManualMigrateRemovePeople migration - Please be patient, this may take some time. This is not an error"); - - context.Person.RemoveRange(context.Person); - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateRemovePeople", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateRemovePeople migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs b/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs deleted file mode 100644 index 4f0ed3f96..000000000 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// When I removed Scrobble support for Book libraries, I forgot to turn the setting off for said libraries. -/// -public static class ManualMigrateUnscrobbleBookLibraries -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateUnscrobbleBookLibraries")) - { - return; - } - - logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Please be patient, this may take some time. This is not an error"); - - var libs = await context.Library.Where(l => l.Type == LibraryType.Book).ToListAsync(); - foreach (var lib in libs) - { - lib.AllowScrobbling = false; - context.Entry(lib).State = EntityState.Modified; - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateUnscrobbleBookLibraries", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs b/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs deleted file mode 100644 index 00233852a..000000000 --- a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users. -/// -public static class MigrateLowestSeriesFolderPath2 -{ - public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger) - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath2")) - { - return; - } - - logger.LogCritical( - "Running MigrateLowestSeriesFolderPath2 migration - Please be patient, this may take some time. This is not an error"); - - var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync(); - foreach (var s in series) - { - s.LowestFolderPath = string.Empty; - unitOfWork.SeriesRepository.Update(s); - } - - // Save changes after processing all series - if (dataContext.ChangeTracker.HasChanges()) - { - await dataContext.SaveChangesAsync(); - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateLowestSeriesFolderPath2", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - - await dataContext.SaveChangesAsync(); - logger.LogCritical( - "Running MigrateLowestSeriesFolderPath2 migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs b/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs deleted file mode 100644 index 60fd170fd..000000000 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Entities.Metadata; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series -/// -public static class ManualMigrateBlacklistTableToSeries -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateBlacklistTableToSeries")) - { - return; - } - - logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Please be patient, this may take some time. This is not an error"); - - // Get all series in the Blacklist table and set their IsBlacklist = true - var blacklistedSeries = await context.SeriesBlacklist - .Include(s => s.Series.ExternalSeriesMetadata) - .Select(s => s.Series) - .ToListAsync(); - - foreach (var series in blacklistedSeries) - { - series.IsBlacklisted = true; - series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() { SeriesId = series.Id }; - - if (series.ExternalSeriesMetadata.AniListId > 0) - { - series.IsBlacklisted = false; - logger.LogInformation("{SeriesName} was in Blacklist table, but has valid AniList Id, not blacklisting", series.Name); - } - - context.Series.Entry(series).State = EntityState.Modified; - } - // Remove everything in SeriesBlacklist (it will be removed in another migration) - context.SeriesBlacklist.RemoveRange(context.SeriesBlacklist); - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateBlacklistTableToSeries", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs b/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs deleted file mode 100644 index 14bc57cb1..000000000 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.History; -using API.Entities.Metadata; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.5 - Migrating Kavita+ Series that are Blacklisted but have valid ExternalSeries row -/// -public static class ManualMigrateInvalidBlacklistSeries -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateInvalidBlacklistSeries")) - { - return; - } - - logger.LogCritical("Running ManualMigrateInvalidBlacklistSeries migration - Please be patient, this may take some time. This is not an error"); - - // Get all series in the Blacklist table and set their IsBlacklist = true - var blacklistedSeries = await context.Series - .Include(s => s.ExternalSeriesMetadata) - .Where(s => s.IsBlacklisted && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue) - .ToListAsync(); - - foreach (var series in blacklistedSeries) - { - series.IsBlacklisted = false; - context.Series.Entry(series).State = EntityState.Modified; - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateInvalidBlacklistSeries", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateInvalidBlacklistSeries migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs b/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs deleted file mode 100644 index 30e4a6d8e..000000000 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Manage; -using API.Entities.History; -using API.Entities.Metadata; -using API.Extensions.QueryExtensions; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.5 - After user testing, the needs manual match has some edge cases from migrations and for best user experience, -/// should be reset to allow the upgraded system to process better. -/// -public static class ManualMigrateNeedsManualMatch -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateNeedsManualMatch")) - { - return; - } - - logger.LogCritical("Running ManualMigrateNeedsManualMatch migration - Please be patient, this may take some time. This is not an error"); - - // Get all series in the Blacklist table and set their IsBlacklist = true - var series = await context.Series - .FilterMatchState(MatchStateOption.Error) - .ToListAsync(); - - foreach (var seriesEntry in series) - { - seriesEntry.IsBlacklisted = false; - context.Series.Update(seriesEntry); - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateNeedsManualMatch", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateNeedsManualMatch migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs b/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs deleted file mode 100644 index b0d483de6..000000000 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.5 - There seems to be some scrobble events that are pre-scrobble error table that can be processed over and over. -/// This will take the given year and minus 1 from it and clear everything from that and anything that is errored. -/// -public static class ManualMigrateScrobbleErrors -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleErrors")) - { - return; - } - - logger.LogCritical("Running ManualMigrateScrobbleErrors migration - Please be patient, this may take some time. This is not an error"); - - // Get all series in the Blacklist table and set their IsBlacklist = true - var events = await context.ScrobbleEvent - .Where(se => se.LastModifiedUtc <= DateTime.UtcNow.AddYears(-1) || se.IsErrored) - .ToListAsync(); - - context.ScrobbleEvent.RemoveRange(events); - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - logger.LogInformation("Removed {Count} old scrobble events", events.Count); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateScrobbleErrors", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateScrobbleErrors migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs b/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs deleted file mode 100644 index 67488d337..000000000 --- a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Services; -using CsvHelper; -using CsvHelper.Configuration.Attributes; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - - -/// -/// v0.8.5 - Progress is extracted and saved in a csv since PDF parser has massive changes -/// -public static class MigrateProgressExportForV085 -{ - public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger logger) - { - try - { - if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateProgressExportForV085")) - { - return; - } - - logger.LogCritical( - "Running MigrateProgressExportForV085 migration - Please be patient, this may take some time. This is not an error"); - - var data = await dataContext.AppUserProgresses - .Join(dataContext.Series, progress => progress.SeriesId, series => series.Id, (progress, series) => new { progress, series }) - .Join(dataContext.Volume, ps => ps.progress.VolumeId, volume => volume.Id, (ps, volume) => new { ps.progress, ps.series, volume }) - .Join(dataContext.Chapter, psv => psv.progress.ChapterId, chapter => chapter.Id, (psv, chapter) => new { psv.progress, psv.series, psv.volume, chapter }) - .Join(dataContext.MangaFile, psvc => psvc.chapter.Id, mangaFile => mangaFile.ChapterId, (psvc, mangaFile) => new { psvc.progress, psvc.series, psvc.volume, psvc.chapter, mangaFile }) - .Join(dataContext.AppUser, psvcm => psvcm.progress.AppUserId, appUser => appUser.Id, (psvcm, appUser) => new - { - LibraryId = psvcm.series.LibraryId, - LibraryName = psvcm.series.Library.Name, - SeriesName = psvcm.series.Name, - VolumeRange = psvcm.volume.MinNumber + "-" + psvcm.volume.MaxNumber, - VolumeLookupName = psvcm.volume.Name, - ChapterRange = psvcm.chapter.Range, - MangaFileName = psvcm.mangaFile.FileName, - MangaFilePath = psvcm.mangaFile.FilePath, - AppUserName = appUser.UserName, - AppUserId = appUser.Id, - PagesRead = psvcm.progress.PagesRead, - BookScrollId = psvcm.progress.BookScrollId, - ProgressCreated = psvcm.progress.Created, - ProgressLastModified = psvcm.progress.LastModified - }).ToListAsync(); - - - // Write the mapped data to a CSV file - await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "progress_export-v0.8.5.csv")); - await using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); - await csv.WriteRecordsAsync(data); - - logger.LogCritical( - "Running MigrateProgressExportForV085 migration - Completed. This is not an error"); - } - catch (Exception ex) - { - // On new installs, the db isn't setup yet, so this has nothing to do - } - - dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() - { - Name = "MigrateProgressExportForV085", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await dataContext.SaveChangesAsync(); - } -} diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs b/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs deleted file mode 100644 index eb51d0fe6..000000000 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.6 - Manually check when a user triggers scrobble event generation -/// -public static class ManualMigrateScrobbleEventGen -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleEventGen")) - { - return; - } - - logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Please be patient, this may take some time. This is not an error"); - - var users = await context.Users - .Where(u => u.AniListAccessToken != null) - .ToListAsync(); - - foreach (var user in users) - { - if (await context.ScrobbleEvent.AnyAsync(se => se.AppUserId == user.Id)) - { - user.HasRunScrobbleEventGeneration = true; - user.ScrobbleEventGenerationRan = DateTime.UtcNow; - context.AppUser.Update(user); - } - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateScrobbleEventGen", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs b/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs deleted file mode 100644 index 4749ff2ec..000000000 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.8.6 - Change to not scrobble specials as they will never process, this migration removes all existing scrobble events -/// -public static class ManualMigrateScrobbleSpecials -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleSpecials")) - { - return; - } - - logger.LogCritical("Running ManualMigrateScrobbleSpecials migration - Please be patient, this may take some time. This is not an error"); - - // Get all series in the Blacklist table and set their IsBlacklist = true - var events = await context.ScrobbleEvent - .Where(se => se.VolumeNumber == Parser.SpecialVolumeNumber) - .ToListAsync(); - - context.ScrobbleEvent.RemoveRange(events); - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - logger.LogInformation("Removed {Count} scrobble events that were specials", events.Count); - } - - await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() - { - Name = "ManualMigrateScrobbleSpecials", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow - }); - await context.SaveChangesAsync(); - - logger.LogCritical("Running ManualMigrateScrobbleSpecials migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs b/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs deleted file mode 100644 index b2afde98a..000000000 --- a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public static class ManualMigrateReadingProfiles -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateReadingProfiles")) - { - return; - } - - logger.LogCritical("Running ManualMigrateReadingProfiles migration - Please be patient, this may take some time. This is not an error"); - - var users = await context.AppUser - .Include(u => u.UserPreferences) - .Include(u => u.ReadingProfiles) - .ToListAsync(); - - foreach (var user in users) - { - var readingProfile = new AppUserReadingProfile - { - Name = "Default", - NormalizedName = "Default".ToNormalized(), - Kind = ReadingProfileKind.Default, - LibraryIds = [], - SeriesIds = [], - BackgroundColor = user.UserPreferences.BackgroundColor, - EmulateBook = user.UserPreferences.EmulateBook, - AppUser = user, - PdfTheme = user.UserPreferences.PdfTheme, - ReaderMode = user.UserPreferences.ReaderMode, - ReadingDirection = user.UserPreferences.ReadingDirection, - ScalingOption = user.UserPreferences.ScalingOption, - LayoutMode = user.UserPreferences.LayoutMode, - WidthOverride = null, - AppUserId = user.Id, - AutoCloseMenu = user.UserPreferences.AutoCloseMenu, - BookReaderMargin = user.UserPreferences.BookReaderMargin, - PageSplitOption = user.UserPreferences.PageSplitOption, - BookThemeName = user.UserPreferences.BookThemeName, - PdfSpreadMode = user.UserPreferences.PdfSpreadMode, - PdfScrollMode = user.UserPreferences.PdfScrollMode, - SwipeToPaginate = user.UserPreferences.SwipeToPaginate, - BookReaderFontFamily = user.UserPreferences.BookReaderFontFamily, - BookReaderFontSize = user.UserPreferences.BookReaderFontSize, - BookReaderImmersiveMode = user.UserPreferences.BookReaderImmersiveMode, - BookReaderLayoutMode = user.UserPreferences.BookReaderLayoutMode, - BookReaderLineSpacing = user.UserPreferences.BookReaderLineSpacing, - BookReaderReadingDirection = user.UserPreferences.BookReaderReadingDirection, - BookReaderWritingStyle = user.UserPreferences.BookReaderWritingStyle, - AllowAutomaticWebtoonReaderDetection = user.UserPreferences.AllowAutomaticWebtoonReaderDetection, - BookReaderTapToPaginate = user.UserPreferences.BookReaderTapToPaginate, - ShowScreenHints = user.UserPreferences.ShowScreenHints, - }; - user.ReadingProfiles.Add(readingProfile); - } - - await context.SaveChangesAsync(); - - context.ManualMigrationHistory.Add(new ManualMigrationHistory - { - Name = "ManualMigrateReadingProfiles", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow, - }); - await context.SaveChangesAsync(); - - - logger.LogCritical("Running ManualMigrateReadingProfiles migration - Completed. This is not an error"); - - } -} diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 8a9ef1900..49fb24474 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -9,7 +9,6 @@ using Kavita.Common.Extensions; using Nager.ArticleNumber; namespace API.Data.Metadata; -#nullable enable /// /// A representation of a ComicInfo.xml file @@ -127,11 +126,7 @@ public class ComicInfo public string CoverArtist { get; set; } = string.Empty; public string Editor { get; set; } = string.Empty; public string Publisher { get; set; } = string.Empty; - public string Imprint { get; set; } = string.Empty; public string Characters { get; set; } = string.Empty; - public string Teams { get; set; } = string.Empty; - public string Locations { get; set; } = string.Empty; - public static AgeRating ConvertAgeRatingToEnum(string value) { @@ -155,12 +150,9 @@ public class ComicInfo info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer); info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller); info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher); - info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint); info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters); info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator); info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist); - info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams); - info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations); // We need to convert GTIN to ISBN if (!string.IsNullOrEmpty(info.GTIN)) @@ -181,12 +173,7 @@ public class ComicInfo if (!string.IsNullOrEmpty(info.Number)) { - info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes - } - - if (!string.IsNullOrEmpty(info.Volume)) - { - info.Volume = info.Volume.Trim(); + info.Number = info.Number.Replace(",", "."); // Corrective measure for non English OSes } } diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs b/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs deleted file mode 100644 index ec955717c..000000000 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs +++ /dev/null @@ -1,2504 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20231113215006_LibraryFileTypes")] - partial class LibraryFileTypes - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs b/API/Data/Migrations/20231113215006_LibraryFileTypes.cs deleted file mode 100644 index 7fed106e7..000000000 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class LibraryFileTypes : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "LibraryFileTypeGroup", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - LibraryId = table.Column(type: "INTEGER", nullable: false), - FileTypeGroup = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_LibraryFileTypeGroup", x => x.Id); - table.ForeignKey( - name: "FK_LibraryFileTypeGroup_Library_LibraryId", - column: x => x.LibraryId, - principalTable: "Library", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_LibraryFileTypeGroup_LibraryId", - table: "LibraryFileTypeGroup", - column: "LibraryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "LibraryFileTypeGroup"); - } - } -} diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs deleted file mode 100644 index b53aa8138..000000000 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs +++ /dev/null @@ -1,2536 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20231117234829_LibraryExcludePatterns")] - partial class LibraryExcludePatterns - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs deleted file mode 100644 index d1dd084f7..000000000 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class LibraryExcludePatterns : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "LibraryExcludePattern", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Pattern = table.Column(type: "TEXT", nullable: true), - LibraryId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_LibraryExcludePattern", x => x.Id); - table.ForeignKey( - name: "FK_LibraryExcludePattern_Library_LibraryId", - column: x => x.LibraryId, - principalTable: "Library", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_LibraryExcludePattern_LibraryId", - table: "LibraryExcludePattern", - column: "LibraryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "LibraryExcludePattern"); - } - } -} diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs deleted file mode 100644 index e7fdad65e..000000000 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs +++ /dev/null @@ -1,2787 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240121223643_ExternalSeriesMetadata")] - partial class ExternalSeriesMetadata - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("LastUpdatedUtc") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs deleted file mode 100644 index 718332b9f..000000000 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ExternalSeriesMetadata : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ExternalRating", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - AverageScore = table.Column(type: "INTEGER", nullable: false), - FavoriteCount = table.Column(type: "INTEGER", nullable: false), - Provider = table.Column(type: "INTEGER", nullable: false), - ProviderUrl = table.Column(type: "TEXT", nullable: true), - SeriesId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalRating", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ExternalRecommendation", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true), - CoverUrl = table.Column(type: "TEXT", nullable: true), - Url = table.Column(type: "TEXT", nullable: true), - Summary = table.Column(type: "TEXT", nullable: true), - AniListId = table.Column(type: "INTEGER", nullable: true), - MalId = table.Column(type: "INTEGER", nullable: true), - Provider = table.Column(type: "INTEGER", nullable: false), - SeriesId = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalRecommendation", x => x.Id); - table.ForeignKey( - name: "FK_ExternalRecommendation_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ExternalReview", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Tagline = table.Column(type: "TEXT", nullable: true), - Body = table.Column(type: "TEXT", nullable: true), - BodyJustText = table.Column(type: "TEXT", nullable: true), - RawBody = table.Column(type: "TEXT", nullable: true), - Provider = table.Column(type: "INTEGER", nullable: false), - SiteUrl = table.Column(type: "TEXT", nullable: true), - Username = table.Column(type: "TEXT", nullable: true), - Rating = table.Column(type: "INTEGER", nullable: false), - Score = table.Column(type: "INTEGER", nullable: false), - TotalVotes = table.Column(type: "INTEGER", nullable: false), - SeriesId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalReview", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ExternalSeriesMetadata", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - AverageExternalRating = table.Column(type: "INTEGER", nullable: false), - AniListId = table.Column(type: "INTEGER", nullable: false), - MalId = table.Column(type: "INTEGER", nullable: false), - GoogleBooksId = table.Column(type: "TEXT", nullable: true), - LastUpdatedUtc = table.Column(type: "TEXT", nullable: false), - SeriesId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalSeriesMetadata", x => x.Id); - table.ForeignKey( - name: "FK_ExternalSeriesMetadata_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ExternalRatingExternalSeriesMetadata", - columns: table => new - { - ExternalRatingsId = table.Column(type: "INTEGER", nullable: false), - ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalRatingExternalSeriesMetadata", x => new { x.ExternalRatingsId, x.ExternalSeriesMetadatasId }); - table.ForeignKey( - name: "FK_ExternalRatingExternalSeriesMetadata_ExternalRating_ExternalRatingsId", - column: x => x.ExternalRatingsId, - principalTable: "ExternalRating", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", - column: x => x.ExternalSeriesMetadatasId, - principalTable: "ExternalSeriesMetadata", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ExternalRecommendationExternalSeriesMetadata", - columns: table => new - { - ExternalRecommendationsId = table.Column(type: "INTEGER", nullable: false), - ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalRecommendationExternalSeriesMetadata", x => new { x.ExternalRecommendationsId, x.ExternalSeriesMetadatasId }); - table.ForeignKey( - name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalRecommendation_ExternalRecommendationsId", - column: x => x.ExternalRecommendationsId, - principalTable: "ExternalRecommendation", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", - column: x => x.ExternalSeriesMetadatasId, - principalTable: "ExternalSeriesMetadata", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ExternalReviewExternalSeriesMetadata", - columns: table => new - { - ExternalReviewsId = table.Column(type: "INTEGER", nullable: false), - ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ExternalReviewExternalSeriesMetadata", x => new { x.ExternalReviewsId, x.ExternalSeriesMetadatasId }); - table.ForeignKey( - name: "FK_ExternalReviewExternalSeriesMetadata_ExternalReview_ExternalReviewsId", - column: x => x.ExternalReviewsId, - principalTable: "ExternalReview", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", - column: x => x.ExternalSeriesMetadatasId, - principalTable: "ExternalSeriesMetadata", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadatasId", - table: "ExternalRatingExternalSeriesMetadata", - column: "ExternalSeriesMetadatasId"); - - migrationBuilder.CreateIndex( - name: "IX_ExternalRecommendation_SeriesId", - table: "ExternalRecommendation", - column: "SeriesId"); - - migrationBuilder.CreateIndex( - name: "IX_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadatasId", - table: "ExternalRecommendationExternalSeriesMetadata", - column: "ExternalSeriesMetadatasId"); - - migrationBuilder.CreateIndex( - name: "IX_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadatasId", - table: "ExternalReviewExternalSeriesMetadata", - column: "ExternalSeriesMetadatasId"); - - migrationBuilder.CreateIndex( - name: "IX_ExternalSeriesMetadata_SeriesId", - table: "ExternalSeriesMetadata", - column: "SeriesId", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ExternalRatingExternalSeriesMetadata"); - - migrationBuilder.DropTable( - name: "ExternalRecommendationExternalSeriesMetadata"); - - migrationBuilder.DropTable( - name: "ExternalReviewExternalSeriesMetadata"); - - migrationBuilder.DropTable( - name: "ExternalRating"); - - migrationBuilder.DropTable( - name: "ExternalRecommendation"); - - migrationBuilder.DropTable( - name: "ExternalReview"); - - migrationBuilder.DropTable( - name: "ExternalSeriesMetadata"); - } - } -} diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs deleted file mode 100644 index 730b40ec0..000000000 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs +++ /dev/null @@ -1,2793 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240128153433_VolumeMinMaxNumbers")] - partial class VolumeMinMaxNumbers - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("LastUpdatedUtc") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs deleted file mode 100644 index 491fd057f..000000000 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class VolumeMinMaxNumbers : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MaxNumber", - table: "Volume", - type: "REAL", - nullable: false, - defaultValue: 0f); - - migrationBuilder.AddColumn( - name: "MinNumber", - table: "Volume", - type: "REAL", - nullable: false, - defaultValue: 0f); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MaxNumber", - table: "Volume"); - - migrationBuilder.DropColumn( - name: "MinNumber", - table: "Volume"); - } - } -} diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs b/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs deleted file mode 100644 index a4203171c..000000000 --- a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs +++ /dev/null @@ -1,2844 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240130190617_WantToReadFix")] - partial class WantToReadFix - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("LastUpdatedUtc") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.cs b/API/Data/Migrations/20240130190617_WantToReadFix.cs deleted file mode 100644 index 386160db3..000000000 --- a/API/Data/Migrations/20240130190617_WantToReadFix.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class WantToReadFix : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Series_AspNetUsers_AppUserId", - table: "Series"); - - migrationBuilder.DropIndex( - name: "IX_Series_AppUserId", - table: "Series"); - - migrationBuilder.DropColumn( - name: "AppUserId", - table: "Series"); - - migrationBuilder.CreateTable( - name: "AppUserWantToRead", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - SeriesId = table.Column(type: "INTEGER", nullable: false), - AppUserId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserWantToRead", x => x.Id); - table.ForeignKey( - name: "FK_AppUserWantToRead_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserWantToRead_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ManualMigrationHistory", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ProductVersion = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: true), - RanAt = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ManualMigrationHistory", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserWantToRead_AppUserId", - table: "AppUserWantToRead", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserWantToRead_SeriesId", - table: "AppUserWantToRead", - column: "SeriesId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AppUserWantToRead"); - - migrationBuilder.DropTable( - name: "ManualMigrationHistory"); - - migrationBuilder.AddColumn( - name: "AppUserId", - table: "Series", - type: "INTEGER", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_Series_AppUserId", - table: "Series", - column: "AppUserId"); - - migrationBuilder.AddForeignKey( - name: "FK_Series_AspNetUsers_AppUserId", - table: "Series", - column: "AppUserId", - principalTable: "AspNetUsers", - principalColumn: "Id"); - } - } -} diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs b/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs deleted file mode 100644 index c399f13cc..000000000 --- a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs +++ /dev/null @@ -1,2874 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240204141206_BlackListSeries")] - partial class BlackListSeries - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.cs b/API/Data/Migrations/20240204141206_BlackListSeries.cs deleted file mode 100644 index 9e051e5a7..000000000 --- a/API/Data/Migrations/20240204141206_BlackListSeries.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class BlackListSeries : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "LastUpdatedUtc", - table: "ExternalSeriesMetadata", - newName: "ValidUntilUtc"); - - migrationBuilder.CreateTable( - name: "SeriesBlacklist", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - SeriesId = table.Column(type: "INTEGER", nullable: false), - LastChecked = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SeriesBlacklist", x => x.Id); - table.ForeignKey( - name: "FK_SeriesBlacklist_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_SeriesBlacklist_SeriesId", - table: "SeriesBlacklist", - column: "SeriesId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SeriesBlacklist"); - - migrationBuilder.RenameColumn( - name: "ValidUntilUtc", - table: "ExternalSeriesMetadata", - newName: "LastUpdatedUtc"); - } - } -} diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs b/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs deleted file mode 100644 index df5692eb4..000000000 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs +++ /dev/null @@ -1,2880 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240205184724_ScrobbleEventError")] - partial class ScrobbleEventError - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs b/API/Data/Migrations/20240205184724_ScrobbleEventError.cs deleted file mode 100644 index 5c8071b18..000000000 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ScrobbleEventError : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "VolumeNumber", - table: "ScrobbleEvent", - type: "REAL", - nullable: true, - oldClrType: typeof(int), - oldType: "INTEGER", - oldNullable: true); - - migrationBuilder.AddColumn( - name: "ErrorDetails", - table: "ScrobbleEvent", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "IsErrored", - table: "ScrobbleEvent", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ErrorDetails", - table: "ScrobbleEvent"); - - migrationBuilder.DropColumn( - name: "IsErrored", - table: "ScrobbleEvent"); - - migrationBuilder.AlterColumn( - name: "VolumeNumber", - table: "ScrobbleEvent", - type: "INTEGER", - nullable: true, - oldClrType: typeof(float), - oldType: "REAL", - oldNullable: true); - } - } -} diff --git a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs b/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs deleted file mode 100644 index 0afb2e5cb..000000000 --- a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs +++ /dev/null @@ -1,2871 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240209224347_DBTweaks")] - partial class DBTweaks - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240209224347_DBTweaks.cs b/API/Data/Migrations/20240209224347_DBTweaks.cs deleted file mode 100644 index 797905930..000000000 --- a/API/Data/Migrations/20240209224347_DBTweaks.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class DBTweaks : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_ExternalRecommendation_Series_SeriesId", - table: "ExternalRecommendation"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddForeignKey( - name: "FK_ExternalRecommendation_Series_SeriesId", - table: "ExternalRecommendation", - column: "SeriesId", - principalTable: "Series", - principalColumn: "Id"); - } - } -} diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs b/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs deleted file mode 100644 index d770ccbbd..000000000 --- a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs +++ /dev/null @@ -1,2877 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240214232436_ChapterNumber")] - partial class ChapterNumber - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.cs b/API/Data/Migrations/20240214232436_ChapterNumber.cs deleted file mode 100644 index c1e277d58..000000000 --- a/API/Data/Migrations/20240214232436_ChapterNumber.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterNumber : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MaxNumber", - table: "Chapter", - type: "REAL", - nullable: false, - defaultValue: 0f); - - migrationBuilder.AddColumn( - name: "MinNumber", - table: "Chapter", - type: "REAL", - nullable: false, - defaultValue: 0f); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MaxNumber", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "MinNumber", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs b/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs deleted file mode 100644 index 7709d9afa..000000000 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs +++ /dev/null @@ -1,2880 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240216000223_MangaFileNameTemp")] - partial class MangaFileNameTemp - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs b/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs deleted file mode 100644 index 8a14c912c..000000000 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class MangaFileNameTemp : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "FileName", - table: "MangaFile", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "FileName", - table: "MangaFile"); - } - } -} diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs b/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs deleted file mode 100644 index 68c1a12e5..000000000 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs +++ /dev/null @@ -1,2883 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240222125420_ChapterIssueSort")] - partial class ChapterIssueSort - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs b/API/Data/Migrations/20240222125420_ChapterIssueSort.cs deleted file mode 100644 index 0689a8e88..000000000 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterIssueSort : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "SortOrder", - table: "Chapter", - type: "REAL", - nullable: false, - defaultValue: 0f); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "SortOrder", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs b/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs deleted file mode 100644 index c7f646f73..000000000 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs +++ /dev/null @@ -1,2886 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240225235816_VolumeLookupName")] - partial class VolumeLookupName - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.cs b/API/Data/Migrations/20240225235816_VolumeLookupName.cs deleted file mode 100644 index 3d42e9645..000000000 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class VolumeLookupName : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LookupName", - table: "Volume", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LookupName", - table: "Volume"); - } - } -} diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs b/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs deleted file mode 100644 index d99650e86..000000000 --- a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs +++ /dev/null @@ -1,2889 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240309140117_SeriesImprints")] - partial class SeriesImprints - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.cs b/API/Data/Migrations/20240309140117_SeriesImprints.cs deleted file mode 100644 index a48ac7c48..000000000 --- a/API/Data/Migrations/20240309140117_SeriesImprints.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class SeriesImprints : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ImprintLocked", - table: "SeriesMetadata", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ImprintLocked", - table: "SeriesMetadata"); - } - } -} diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs b/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs deleted file mode 100644 index 707d6ea0a..000000000 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs +++ /dev/null @@ -1,2892 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240313112552_SeriesLowestFolderPath")] - partial class SeriesLowestFolderPath - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs b/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs deleted file mode 100644 index e138bd8f1..000000000 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class SeriesLowestFolderPath : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LowestFolderPath", - table: "Series", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LowestFolderPath", - table: "Series"); - } - } -} diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs b/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs deleted file mode 100644 index 21616f684..000000000 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs +++ /dev/null @@ -1,2898 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240314194402_TeamsAndLocations")] - partial class TeamsAndLocations - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs b/API/Data/Migrations/20240314194402_TeamsAndLocations.cs deleted file mode 100644 index dca377c99..000000000 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class TeamsAndLocations : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LocationLocked", - table: "SeriesMetadata", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "TeamLocked", - table: "SeriesMetadata", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LocationLocked", - table: "SeriesMetadata"); - - migrationBuilder.DropColumn( - name: "TeamLocked", - table: "SeriesMetadata"); - } - } -} diff --git a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs b/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs deleted file mode 100644 index ee182676d..000000000 --- a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs +++ /dev/null @@ -1,2904 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240321173812_UserMalToken")] - partial class UserMalToken - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240321173812_UserMalToken.cs b/API/Data/Migrations/20240321173812_UserMalToken.cs deleted file mode 100644 index f1b1d3caa..000000000 --- a/API/Data/Migrations/20240321173812_UserMalToken.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class UserMalToken : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MalAccessToken", - table: "AspNetUsers", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "MalUserName", - table: "AspNetUsers", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MalAccessToken", - table: "AspNetUsers"); - - migrationBuilder.DropColumn( - name: "MalUserName", - table: "AspNetUsers"); - } - } -} diff --git a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs b/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs deleted file mode 100644 index cba2d534f..000000000 --- a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs +++ /dev/null @@ -1,2916 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240328130057_PdfSettings")] - partial class PdfSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240328130057_PdfSettings.cs b/API/Data/Migrations/20240328130057_PdfSettings.cs deleted file mode 100644 index 699875968..000000000 --- a/API/Data/Migrations/20240328130057_PdfSettings.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PdfSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "PdfLayoutMode", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "PdfScrollMode", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "PdfSpreadMode", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "PdfTheme", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "PdfLayoutMode", - table: "AppUserPreferences"); - - migrationBuilder.DropColumn( - name: "PdfScrollMode", - table: "AppUserPreferences"); - - migrationBuilder.DropColumn( - name: "PdfSpreadMode", - table: "AppUserPreferences"); - - migrationBuilder.DropColumn( - name: "PdfTheme", - table: "AppUserPreferences"); - } - } -} diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs b/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs deleted file mode 100644 index 5527a0fbb..000000000 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs +++ /dev/null @@ -1,3019 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240331172900_UserBasedCollections")] - partial class UserBasedCollections - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.cs b/API/Data/Migrations/20240331172900_UserBasedCollections.cs deleted file mode 100644 index c5a376bd8..000000000 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class UserBasedCollections : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AppUserCollection", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", nullable: true), - NormalizedTitle = table.Column(type: "TEXT", nullable: true), - Summary = table.Column(type: "TEXT", nullable: true), - Promoted = table.Column(type: "INTEGER", nullable: false), - CoverImage = table.Column(type: "TEXT", nullable: true), - CoverImageLocked = table.Column(type: "INTEGER", nullable: false), - AgeRating = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), - Created = table.Column(type: "TEXT", nullable: false), - LastModified = table.Column(type: "TEXT", nullable: false), - CreatedUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false), - LastSyncUtc = table.Column(type: "TEXT", nullable: false), - Source = table.Column(type: "INTEGER", nullable: false), - SourceUrl = table.Column(type: "TEXT", nullable: true), - AppUserId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserCollection", x => x.Id); - table.ForeignKey( - name: "FK_AppUserCollection_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AppUserCollectionSeries", - columns: table => new - { - CollectionsId = table.Column(type: "INTEGER", nullable: false), - ItemsId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserCollectionSeries", x => new { x.CollectionsId, x.ItemsId }); - table.ForeignKey( - name: "FK_AppUserCollectionSeries_AppUserCollection_CollectionsId", - column: x => x.CollectionsId, - principalTable: "AppUserCollection", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserCollectionSeries_Series_ItemsId", - column: x => x.ItemsId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserCollection_AppUserId", - table: "AppUserCollection", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserCollectionSeries_ItemsId", - table: "AppUserCollectionSeries", - column: "ItemsId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AppUserCollectionSeries"); - - migrationBuilder.DropTable( - name: "AppUserCollection"); - } - } -} diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs b/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs deleted file mode 100644 index 3cd3291b2..000000000 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs +++ /dev/null @@ -1,3019 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240418163829_ChapterSortOrderLock")] - partial class ChapterSortOrderLock - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs b/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs deleted file mode 100644 index 197085b0c..000000000 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterSortOrderLock : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "PdfLayoutMode", - table: "AppUserPreferences"); - - migrationBuilder.AddColumn( - name: "SortOrderLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "SortOrderLocked", - table: "Chapter"); - - migrationBuilder.AddColumn( - name: "PdfLayoutMode", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - } -} diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs b/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs deleted file mode 100644 index 1dff0c0e5..000000000 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs +++ /dev/null @@ -1,3025 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240503120147_SmartCollectionFields")] - partial class SmartCollectionFields - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs b/API/Data/Migrations/20240503120147_SmartCollectionFields.cs deleted file mode 100644 index f0b6ed693..000000000 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class SmartCollectionFields : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MissingSeriesFromSource", - table: "AppUserCollection", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "TotalSourceCount", - table: "AppUserCollection", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MissingSeriesFromSource", - table: "AppUserCollection"); - - migrationBuilder.DropColumn( - name: "TotalSourceCount", - table: "AppUserCollection"); - } - } -} diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs b/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs deleted file mode 100644 index c88a1628f..000000000 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs +++ /dev/null @@ -1,3043 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240510134030_SiteThemeFields")] - partial class SiteThemeFields - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.cs b/API/Data/Migrations/20240510134030_SiteThemeFields.cs deleted file mode 100644 index 36171fa0a..000000000 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class SiteThemeFields : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Author", - table: "SiteTheme", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "CompatibleVersion", - table: "SiteTheme", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "Description", - table: "SiteTheme", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "GitHubPath", - table: "SiteTheme", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PreviewUrls", - table: "SiteTheme", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "ShaHash", - table: "SiteTheme", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Author", - table: "SiteTheme"); - - migrationBuilder.DropColumn( - name: "CompatibleVersion", - table: "SiteTheme"); - - migrationBuilder.DropColumn( - name: "Description", - table: "SiteTheme"); - - migrationBuilder.DropColumn( - name: "GitHubPath", - table: "SiteTheme"); - - migrationBuilder.DropColumn( - name: "PreviewUrls", - table: "SiteTheme"); - - migrationBuilder.DropColumn( - name: "ShaHash", - table: "SiteTheme"); - } - } -} diff --git a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs b/API/Data/Migrations/20240704144224_PersonFields.Designer.cs deleted file mode 100644 index ddc41d811..000000000 --- a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs +++ /dev/null @@ -1,3064 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240704144224_PersonFields")] - partial class PersonFields - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240704144224_PersonFields.cs b/API/Data/Migrations/20240704144224_PersonFields.cs deleted file mode 100644 index 2d30696ce..000000000 --- a/API/Data/Migrations/20240704144224_PersonFields.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PersonFields : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AniListId", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "Asin", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "CoverImage", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "CoverImageLocked", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "Description", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "HardcoverId", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "MalId", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: 0L); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AniListId", - table: "Person"); - - migrationBuilder.DropColumn( - name: "Asin", - table: "Person"); - - migrationBuilder.DropColumn( - name: "CoverImage", - table: "Person"); - - migrationBuilder.DropColumn( - name: "CoverImageLocked", - table: "Person"); - - migrationBuilder.DropColumn( - name: "Description", - table: "Person"); - - migrationBuilder.DropColumn( - name: "HardcoverId", - table: "Person"); - - migrationBuilder.DropColumn( - name: "MalId", - table: "Person"); - } - } -} diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs b/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs deleted file mode 100644 index d105ece92..000000000 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs +++ /dev/null @@ -1,3079 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240808100353_CoverPrimaryColors")] - partial class CoverPrimaryColors - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs b/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs deleted file mode 100644 index c69c906b0..000000000 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class CoverPrimaryColors : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "Volume", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "Volume", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "Series", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "Series", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "ReadingList", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "ReadingList", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "Library", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "Library", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "Chapter", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "Chapter", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "AppUserCollection", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "AppUserCollection", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "Volume"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "Volume"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "Series"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "Series"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "ReadingList"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "ReadingList"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "Library"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "Library"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "AppUserCollection"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "AppUserCollection"); - } - } -} diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs b/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs deleted file mode 100644 index 07723e833..000000000 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs +++ /dev/null @@ -1,3142 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240811154857_ChapterMetadataLocks")] - partial class ChapterMetadataLocks - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs b/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs deleted file mode 100644 index b0b58b3b3..000000000 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs +++ /dev/null @@ -1,249 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterMetadataLocks : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AgeRatingLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "CharacterLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ColoristLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "CoverArtistLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EditorLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "GenresLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ISBNLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ImprintLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "InkerLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "LanguageLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "LettererLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "LocationLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "PencillerLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "PublisherLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ReleaseDateLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "SummaryLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "TagsLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "TeamLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "TitleNameLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "TranslatorLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "WriterLocked", - table: "Chapter", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AgeRatingLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "CharacterLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "ColoristLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "CoverArtistLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "EditorLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "GenresLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "ISBNLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "ImprintLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "InkerLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "LanguageLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "LettererLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "LocationLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "PencillerLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "PublisherLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "ReleaseDateLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "SummaryLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "TagsLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "TeamLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "TitleNameLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "TranslatorLocked", - table: "Chapter"); - - migrationBuilder.DropColumn( - name: "WriterLocked", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs b/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs deleted file mode 100644 index 1471c1de7..000000000 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs +++ /dev/null @@ -1,3145 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240813194728_VolumeCoverLocked")] - partial class VolumeCoverLocked - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs b/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs deleted file mode 100644 index c9127ae6a..000000000 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class VolumeCoverLocked : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "CoverImageLocked", - table: "Volume", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "CoverImageLocked", - table: "Volume"); - } - } -} diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs b/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs deleted file mode 100644 index f9b858de5..000000000 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs +++ /dev/null @@ -1,3145 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240917180034_AvgReadingTimeFloat")] - partial class AvgReadingTimeFloat - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.Property("ChapterMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterMetadatasId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.ToTable("ChapterPerson"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.Property("PeopleId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("PeopleId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("PersonSeriesMetadata"); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterPerson", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChapterMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("PersonSeriesMetadata", b => - { - b.HasOne("API.Entities.Person", null) - .WithMany() - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs b/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs deleted file mode 100644 index 70e9238ec..000000000 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class AvgReadingTimeFloat : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Volume", - type: "REAL", - nullable: false, - oldClrType: typeof(int), - oldType: "INTEGER"); - - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Series", - type: "REAL", - nullable: false, - oldClrType: typeof(int), - oldType: "INTEGER"); - - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Chapter", - type: "REAL", - nullable: false, - oldClrType: typeof(int), - oldType: "INTEGER"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Volume", - type: "INTEGER", - nullable: false, - oldClrType: typeof(float), - oldType: "REAL"); - - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Series", - type: "INTEGER", - nullable: false, - oldClrType: typeof(float), - oldType: "REAL"); - - migrationBuilder.AlterColumn( - name: "AvgHoursToRead", - table: "Chapter", - type: "INTEGER", - nullable: false, - oldClrType: typeof(float), - oldType: "REAL"); - } - } -} diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs b/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs deleted file mode 100644 index 3865e6007..000000000 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs +++ /dev/null @@ -1,3170 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20241011143144_PeopleOverhaulPart1")] - partial class PeopleOverhaulPart1 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs b/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs deleted file mode 100644 index 1bf0cf6c4..000000000 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs +++ /dev/null @@ -1,159 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PeopleOverhaulPart1 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ChapterPerson"); - - migrationBuilder.DropTable( - name: "PersonSeriesMetadata"); - - migrationBuilder.DropColumn( - name: "Role", - table: "Person"); - - migrationBuilder.CreateTable( - name: "ChapterPeople", - columns: table => new - { - ChapterId = table.Column(type: "INTEGER", nullable: false), - PersonId = table.Column(type: "INTEGER", nullable: false), - Role = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ChapterPeople", x => new { x.ChapterId, x.PersonId, x.Role }); - table.ForeignKey( - name: "FK_ChapterPeople_Chapter_ChapterId", - column: x => x.ChapterId, - principalTable: "Chapter", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ChapterPeople_Person_PersonId", - column: x => x.PersonId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SeriesMetadataPeople", - columns: table => new - { - SeriesMetadataId = table.Column(type: "INTEGER", nullable: false), - PersonId = table.Column(type: "INTEGER", nullable: false), - Role = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SeriesMetadataPeople", x => new { x.SeriesMetadataId, x.PersonId, x.Role }); - table.ForeignKey( - name: "FK_SeriesMetadataPeople_Person_PersonId", - column: x => x.PersonId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SeriesMetadataPeople_SeriesMetadata_SeriesMetadataId", - column: x => x.SeriesMetadataId, - principalTable: "SeriesMetadata", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ChapterPeople_PersonId", - table: "ChapterPeople", - column: "PersonId"); - - migrationBuilder.CreateIndex( - name: "IX_SeriesMetadataPeople_PersonId", - table: "SeriesMetadataPeople", - column: "PersonId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ChapterPeople"); - - migrationBuilder.DropTable( - name: "SeriesMetadataPeople"); - - migrationBuilder.AddColumn( - name: "Role", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateTable( - name: "ChapterPerson", - columns: table => new - { - ChapterMetadatasId = table.Column(type: "INTEGER", nullable: false), - PeopleId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ChapterPerson", x => new { x.ChapterMetadatasId, x.PeopleId }); - table.ForeignKey( - name: "FK_ChapterPerson_Chapter_ChapterMetadatasId", - column: x => x.ChapterMetadatasId, - principalTable: "Chapter", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ChapterPerson_Person_PeopleId", - column: x => x.PeopleId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PersonSeriesMetadata", - columns: table => new - { - PeopleId = table.Column(type: "INTEGER", nullable: false), - SeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PersonSeriesMetadata", x => new { x.PeopleId, x.SeriesMetadatasId }); - table.ForeignKey( - name: "FK_PersonSeriesMetadata_Person_PeopleId", - column: x => x.PeopleId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PersonSeriesMetadata_SeriesMetadata_SeriesMetadatasId", - column: x => x.SeriesMetadatasId, - principalTable: "SeriesMetadata", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ChapterPerson_PeopleId", - table: "ChapterPerson", - column: "PeopleId"); - - migrationBuilder.CreateIndex( - name: "IX_PersonSeriesMetadata_SeriesMetadatasId", - table: "PersonSeriesMetadata", - column: "SeriesMetadatasId"); - } - } -} diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs b/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs deleted file mode 100644 index bbbf0f989..000000000 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs +++ /dev/null @@ -1,3182 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20241011152321_PeopleOverhaulPart2")] - partial class PeopleOverhaulPart2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs b/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs deleted file mode 100644 index 4fd8e4b8d..000000000 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PeopleOverhaulPart2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "CoverImage", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "CoverImageLocked", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "PrimaryColor", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "SecondaryColor", - table: "Person", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "CoverImage", - table: "Person"); - - migrationBuilder.DropColumn( - name: "CoverImageLocked", - table: "Person"); - - migrationBuilder.DropColumn( - name: "PrimaryColor", - table: "Person"); - - migrationBuilder.DropColumn( - name: "SecondaryColor", - table: "Person"); - } - } -} diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs b/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs deleted file mode 100644 index 6f76df92c..000000000 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs +++ /dev/null @@ -1,3197 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20241011172428_PeopleOverhaulPart3")] - partial class PeopleOverhaulPart3 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs b/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs deleted file mode 100644 index 13aa9e050..000000000 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PeopleOverhaulPart3 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AniListId", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "Asin", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "Description", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "HardcoverId", - table: "Person", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "MalId", - table: "Person", - type: "INTEGER", - nullable: false, - defaultValue: 0L); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AniListId", - table: "Person"); - - migrationBuilder.DropColumn( - name: "Asin", - table: "Person"); - - migrationBuilder.DropColumn( - name: "Description", - table: "Person"); - - migrationBuilder.DropColumn( - name: "HardcoverId", - table: "Person"); - - migrationBuilder.DropColumn( - name: "MalId", - table: "Person"); - } - } -} diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs b/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs deleted file mode 100644 index a5158ebc1..000000000 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs +++ /dev/null @@ -1,3203 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250105180131_SeriesDontMatchAndBlacklist")] - partial class SeriesDontMatchAndBlacklist - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs b/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs deleted file mode 100644 index ab80f0621..000000000 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class SeriesDontMatchAndBlacklist : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DontMatch", - table: "Series", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "IsBlacklisted", - table: "Series", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DontMatch", - table: "Series"); - - migrationBuilder.DropColumn( - name: "IsBlacklisted", - table: "Series"); - } - } -} diff --git a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs b/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs deleted file mode 100644 index ff3212562..000000000 --- a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs +++ /dev/null @@ -1,3265 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250109173537_EmailHistory")] - partial class EmailHistory - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250109173537_EmailHistory.cs b/API/Data/Migrations/20250109173537_EmailHistory.cs deleted file mode 100644 index b31bf20c3..000000000 --- a/API/Data/Migrations/20250109173537_EmailHistory.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class EmailHistory : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "EmailHistory", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Sent = table.Column(type: "INTEGER", nullable: false), - SendDate = table.Column(type: "TEXT", nullable: false), - EmailTemplate = table.Column(type: "TEXT", nullable: true), - Subject = table.Column(type: "TEXT", nullable: true), - Body = table.Column(type: "TEXT", nullable: true), - DeliveryStatus = table.Column(type: "TEXT", nullable: true), - ErrorMessage = table.Column(type: "TEXT", nullable: true), - AppUserId = table.Column(type: "INTEGER", nullable: false), - Created = table.Column(type: "TEXT", nullable: false), - CreatedUtc = table.Column(type: "TEXT", nullable: false), - LastModified = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_EmailHistory", x => x.Id); - table.ForeignKey( - name: "FK_EmailHistory_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_EmailHistory_AppUserId", - table: "EmailHistory", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_EmailHistory_Sent_AppUserId_EmailTemplate_SendDate", - table: "EmailHistory", - columns: new[] { "Sent", "AppUserId", "EmailTemplate", "SendDate" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "EmailHistory"); - } - } -} diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs deleted file mode 100644 index 835510a1e..000000000 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs +++ /dev/null @@ -1,3382 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250202163454_KavitaPlusUserAndMetadataSettings")] - partial class KavitaPlusUserAndMetadataSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs deleted file mode 100644 index b23d7896b..000000000 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class KavitaPlusUserAndMetadataSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AllowMetadataMatching", - table: "Library", - type: "INTEGER", - nullable: false, - defaultValue: true); - - migrationBuilder.AddColumn( - name: "AniListScrobblingEnabled", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: true); - - migrationBuilder.AddColumn( - name: "WantToReadSync", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: true); - - migrationBuilder.CreateTable( - name: "MetadataSettings", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Enabled = table.Column(type: "INTEGER", nullable: false, defaultValue: true), - EnableSummary = table.Column(type: "INTEGER", nullable: false), - EnablePublicationStatus = table.Column(type: "INTEGER", nullable: false), - EnableRelationships = table.Column(type: "INTEGER", nullable: false), - EnablePeople = table.Column(type: "INTEGER", nullable: false), - EnableStartDate = table.Column(type: "INTEGER", nullable: false), - EnableLocalizedName = table.Column(type: "INTEGER", nullable: false), - EnableGenres = table.Column(type: "INTEGER", nullable: false), - EnableTags = table.Column(type: "INTEGER", nullable: false), - FirstLastPeopleNaming = table.Column(type: "INTEGER", nullable: false), - AgeRatingMappings = table.Column(type: "TEXT", nullable: true), - Blacklist = table.Column(type: "TEXT", nullable: true), - Whitelist = table.Column(type: "TEXT", nullable: true), - PersonRoles = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataSettings", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "MetadataFieldMapping", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - SourceType = table.Column(type: "INTEGER", nullable: false), - DestinationType = table.Column(type: "INTEGER", nullable: false), - SourceValue = table.Column(type: "TEXT", nullable: true), - DestinationValue = table.Column(type: "TEXT", nullable: true), - ExcludeFromSource = table.Column(type: "INTEGER", nullable: false), - MetadataSettingsId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MetadataFieldMapping", x => x.Id); - table.ForeignKey( - name: "FK_MetadataFieldMapping_MetadataSettings_MetadataSettingsId", - column: x => x.MetadataSettingsId, - principalTable: "MetadataSettings", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_MetadataFieldMapping_MetadataSettingsId", - table: "MetadataFieldMapping", - column: "MetadataSettingsId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "MetadataFieldMapping"); - - migrationBuilder.DropTable( - name: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "AllowMetadataMatching", - table: "Library"); - - migrationBuilder.DropColumn( - name: "AniListScrobblingEnabled", - table: "AppUserPreferences"); - - migrationBuilder.DropColumn( - name: "WantToReadSync", - table: "AppUserPreferences"); - } - } -} diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs b/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs deleted file mode 100644 index 9aaa63101..000000000 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs +++ /dev/null @@ -1,3398 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250208200843_MoreMetadtaSettings")] - partial class MoreMetadtaSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs b/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs deleted file mode 100644 index 70e42cd11..000000000 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class MoreMetadtaSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "KavitaPlusConnection", - table: "SeriesMetadataPeople", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "OrderWeight", - table: "SeriesMetadataPeople", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "EnableCoverImage", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: true); - - migrationBuilder.AddColumn( - name: "Overrides", - table: "MetadataSettings", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "KavitaPlusConnection", - table: "SeriesMetadataPeople"); - - migrationBuilder.DropColumn( - name: "OrderWeight", - table: "SeriesMetadataPeople"); - - migrationBuilder.DropColumn( - name: "EnableCoverImage", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "Overrides", - table: "MetadataSettings"); - } - } -} diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs b/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs deleted file mode 100644 index be3d5e3f9..000000000 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs +++ /dev/null @@ -1,3403 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250328125012_AutomaticWebtoonReaderMode")] - partial class AutomaticWebtoonReaderMode - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs b/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs deleted file mode 100644 index 38b772811..000000000 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class AutomaticWebtoonReaderMode : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AllowAutomaticWebtoonReaderDetection", - table: "AppUserPreferences", - type: "INTEGER", - nullable: false, - defaultValue: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AllowAutomaticWebtoonReaderDetection", - table: "AppUserPreferences"); - } - } -} diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs b/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs deleted file mode 100644 index 53e450b3b..000000000 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs +++ /dev/null @@ -1,3409 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250408222330_ScrobbleGenerationDbCapture")] - partial class ScrobbleGenerationDbCapture - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs b/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs deleted file mode 100644 index 7431a7338..000000000 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ScrobbleGenerationDbCapture : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "HasRunScrobbleEventGeneration", - table: "AspNetUsers", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ScrobbleEventGenerationRan", - table: "AspNetUsers", - type: "TEXT", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "HasRunScrobbleEventGeneration", - table: "AspNetUsers"); - - migrationBuilder.DropColumn( - name: "ScrobbleEventGenerationRan", - table: "AspNetUsers"); - } - } -} diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs b/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs deleted file mode 100644 index fd287c085..000000000 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs +++ /dev/null @@ -1,3433 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250415194829_KavitaPlusCBR")] - partial class KavitaPlusCBR - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs b/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs deleted file mode 100644 index 188969476..000000000 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class KavitaPlusCBR : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "EnableChapterCoverImage", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableChapterPublisher", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableChapterReleaseDate", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableChapterSummary", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableChapterTitle", - table: "MetadataSettings", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "CbrId", - table: "ExternalSeriesMetadata", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "KavitaPlusConnection", - table: "ChapterPeople", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "OrderWeight", - table: "ChapterPeople", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "EnableChapterCoverImage", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "EnableChapterPublisher", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "EnableChapterReleaseDate", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "EnableChapterSummary", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "EnableChapterTitle", - table: "MetadataSettings"); - - migrationBuilder.DropColumn( - name: "CbrId", - table: "ExternalSeriesMetadata"); - - migrationBuilder.DropColumn( - name: "KavitaPlusConnection", - table: "ChapterPeople"); - - migrationBuilder.DropColumn( - name: "OrderWeight", - table: "ChapterPeople"); - } - } -} diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs b/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs deleted file mode 100644 index 52e2c4a86..000000000 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs +++ /dev/null @@ -1,3536 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250429150140_ChapterRatingAndReviews")] - partial class ChapterRatingAndReviews - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs b/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs deleted file mode 100644 index 5ab51aaba..000000000 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ChapterRatingAndReviews : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Authority", - table: "ExternalReview", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "ChapterId", - table: "ExternalReview", - type: "INTEGER", - nullable: true); - - migrationBuilder.AddColumn( - name: "Authority", - table: "ExternalRating", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "ChapterId", - table: "ExternalRating", - type: "INTEGER", - nullable: true); - - migrationBuilder.AddColumn( - name: "AverageExternalRating", - table: "Chapter", - type: "REAL", - nullable: false, - defaultValue: 0f); - - migrationBuilder.CreateTable( - name: "AppUserChapterRating", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Rating = table.Column(type: "REAL", nullable: false), - HasBeenRated = table.Column(type: "INTEGER", nullable: false), - Review = table.Column(type: "TEXT", nullable: true), - SeriesId = table.Column(type: "INTEGER", nullable: false), - ChapterId = table.Column(type: "INTEGER", nullable: false), - AppUserId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserChapterRating", x => x.Id); - table.ForeignKey( - name: "FK_AppUserChapterRating_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserChapterRating_Chapter_ChapterId", - column: x => x.ChapterId, - principalTable: "Chapter", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AppUserChapterRating_Series_SeriesId", - column: x => x.SeriesId, - principalTable: "Series", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ExternalReview_ChapterId", - table: "ExternalReview", - column: "ChapterId"); - - migrationBuilder.CreateIndex( - name: "IX_ExternalRating_ChapterId", - table: "ExternalRating", - column: "ChapterId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_AppUserId", - table: "AppUserChapterRating", - column: "AppUserId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_ChapterId", - table: "AppUserChapterRating", - column: "ChapterId"); - - migrationBuilder.CreateIndex( - name: "IX_AppUserChapterRating_SeriesId", - table: "AppUserChapterRating", - column: "SeriesId"); - - migrationBuilder.AddForeignKey( - name: "FK_ExternalRating_Chapter_ChapterId", - table: "ExternalRating", - column: "ChapterId", - principalTable: "Chapter", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_ExternalReview_Chapter_ChapterId", - table: "ExternalReview", - column: "ChapterId", - principalTable: "Chapter", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_ExternalRating_Chapter_ChapterId", - table: "ExternalRating"); - - migrationBuilder.DropForeignKey( - name: "FK_ExternalReview_Chapter_ChapterId", - table: "ExternalReview"); - - migrationBuilder.DropTable( - name: "AppUserChapterRating"); - - migrationBuilder.DropIndex( - name: "IX_ExternalReview_ChapterId", - table: "ExternalReview"); - - migrationBuilder.DropIndex( - name: "IX_ExternalRating_ChapterId", - table: "ExternalRating"); - - migrationBuilder.DropColumn( - name: "Authority", - table: "ExternalReview"); - - migrationBuilder.DropColumn( - name: "ChapterId", - table: "ExternalReview"); - - migrationBuilder.DropColumn( - name: "Authority", - table: "ExternalRating"); - - migrationBuilder.DropColumn( - name: "ChapterId", - table: "ExternalRating"); - - migrationBuilder.DropColumn( - name: "AverageExternalRating", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs b/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs deleted file mode 100644 index 5d76571e1..000000000 --- a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs +++ /dev/null @@ -1,3571 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250507221026_PersonAliases")] - partial class PersonAliases - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250507221026_PersonAliases.cs b/API/Data/Migrations/20250507221026_PersonAliases.cs deleted file mode 100644 index cb046a131..000000000 --- a/API/Data/Migrations/20250507221026_PersonAliases.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PersonAliases : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "PersonAlias", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Alias = table.Column(type: "TEXT", nullable: true), - NormalizedAlias = table.Column(type: "TEXT", nullable: true), - PersonId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PersonAlias", x => x.Id); - table.ForeignKey( - name: "FK_PersonAlias_Person_PersonId", - column: x => x.PersonId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_PersonAlias_PersonId", - table: "PersonAlias", - column: "PersonId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PersonAlias"); - } - } -} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs deleted file mode 100644 index 79f6f9504..000000000 --- a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs +++ /dev/null @@ -1,3574 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250519151126_KoreaderHash")] - partial class KoreaderHash - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.cs b/API/Data/Migrations/20250519151126_KoreaderHash.cs deleted file mode 100644 index 006070b72..000000000 --- a/API/Data/Migrations/20250519151126_KoreaderHash.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class KoreaderHash : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "KoreaderHash", - table: "MangaFile", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "KoreaderHash", - table: "MangaFile"); - } - } -} diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs deleted file mode 100644 index 762eae142..000000000 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs +++ /dev/null @@ -1,3698 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250601200056_ReadingProfiles")] - partial class ReadingProfiles - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.PrimitiveCollection("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.cs deleted file mode 100644 index 66b9e53e5..000000000 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ReadingProfiles : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AppUserReadingProfiles", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true), - NormalizedName = table.Column(type: "TEXT", nullable: true), - AppUserId = table.Column(type: "INTEGER", nullable: false), - Kind = table.Column(type: "INTEGER", nullable: false), - LibraryIds = table.Column(type: "TEXT", nullable: true), - SeriesIds = table.Column(type: "TEXT", nullable: true), - ReadingDirection = table.Column(type: "INTEGER", nullable: false), - ScalingOption = table.Column(type: "INTEGER", nullable: false), - PageSplitOption = table.Column(type: "INTEGER", nullable: false), - ReaderMode = table.Column(type: "INTEGER", nullable: false), - AutoCloseMenu = table.Column(type: "INTEGER", nullable: false), - ShowScreenHints = table.Column(type: "INTEGER", nullable: false), - EmulateBook = table.Column(type: "INTEGER", nullable: false), - LayoutMode = table.Column(type: "INTEGER", nullable: false), - BackgroundColor = table.Column(type: "TEXT", nullable: true, defaultValue: "#000000"), - SwipeToPaginate = table.Column(type: "INTEGER", nullable: false), - AllowAutomaticWebtoonReaderDetection = table.Column(type: "INTEGER", nullable: false, defaultValue: true), - WidthOverride = table.Column(type: "INTEGER", nullable: true), - BookReaderMargin = table.Column(type: "INTEGER", nullable: false), - BookReaderLineSpacing = table.Column(type: "INTEGER", nullable: false), - BookReaderFontSize = table.Column(type: "INTEGER", nullable: false), - BookReaderFontFamily = table.Column(type: "TEXT", nullable: true), - BookReaderTapToPaginate = table.Column(type: "INTEGER", nullable: false), - BookReaderReadingDirection = table.Column(type: "INTEGER", nullable: false), - BookReaderWritingStyle = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), - BookThemeName = table.Column(type: "TEXT", nullable: true, defaultValue: "Dark"), - BookReaderLayoutMode = table.Column(type: "INTEGER", nullable: false), - BookReaderImmersiveMode = table.Column(type: "INTEGER", nullable: false), - PdfTheme = table.Column(type: "INTEGER", nullable: false), - PdfScrollMode = table.Column(type: "INTEGER", nullable: false), - PdfSpreadMode = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserReadingProfiles", x => x.Id); - table.ForeignKey( - name: "FK_AppUserReadingProfiles_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserReadingProfiles_AppUserId", - table: "AppUserReadingProfiles", - column: "AppUserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AppUserReadingProfiles"); - } - } -} diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs deleted file mode 100644 index 0e9f00b4e..000000000 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs +++ /dev/null @@ -1,3701 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint")] - partial class AppUserReadingProfileDisableWidthOverrideBreakPoint - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs deleted file mode 100644 index 11a554bdf..000000000 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DisableWidthOverride", - table: "AppUserReadingProfiles", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DisableWidthOverride", - table: "AppUserReadingProfiles"); - } - } -} diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs deleted file mode 100644 index c15f9f77b..000000000 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs +++ /dev/null @@ -1,3709 +0,0 @@ -// -using System; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250620215058_EnableMetadataLibrary")] - partial class EnableMetadataLibrary - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs deleted file mode 100644 index f9e38c01d..000000000 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class EnableMetadataLibrary : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "EnableMetadata", - table: "Library", - type: "INTEGER", - nullable: false, - defaultValue: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "EnableMetadata", - table: "Library"); - } - } -} diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs b/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs deleted file mode 100644 index b72239924..000000000 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs +++ /dev/null @@ -1,3721 +0,0 @@ -// -using System; -using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250626162548_TrackKavitaPlusMetadata")] - partial class TrackKavitaPlusMetadata - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs b/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs deleted file mode 100644 index ac253e0a8..000000000 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class TrackKavitaPlusMetadata : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "KPlusOverrides", - table: "SeriesMetadata", - type: "TEXT", - nullable: true, - defaultValue: "[]"); - - migrationBuilder.AddColumn( - name: "KPlusOverrides", - table: "Chapter", - type: "TEXT", - nullable: true, - defaultValue: "[]"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "KPlusOverrides", - table: "SeriesMetadata"); - - migrationBuilder.DropColumn( - name: "KPlusOverrides", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs deleted file mode 100644 index 165663f3d..000000000 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs +++ /dev/null @@ -1,3724 +0,0 @@ -// -using System; -using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace API.Data.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20250629153840_LibraryRemoveSortPrefix")] - partial class LibraryRemoveSortPrefix - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Page") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserBookmark"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.CollectionTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Id", "Promoted") - .IsUnique(); - - b.ToTable("CollectionTag"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EmailAddress") - .HasColumnType("TEXT"); - - b.Property("IpAddress") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastUsed") - .HasColumnType("TEXT"); - - b.Property("LastUsedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Platform") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("Device"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("FolderPath"); - }); - - modelBuilder.Entity("API.Entities.Genre", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Genre"); - }); - - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AllowScrobbling") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FolderWatching") - .HasColumnType("INTEGER"); - - b.Property("IncludeInDashboard") - .HasColumnType("INTEGER"); - - b.Property("IncludeInRecommended") - .HasColumnType("INTEGER"); - - b.Property("IncludeInSearch") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastScanned") - .HasColumnType("TEXT"); - - b.Property("ManageCollections") - .HasColumnType("INTEGER"); - - b.Property("ManageReadingLists") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("RemovePrefixForSortName") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysis") - .HasColumnType("TEXT"); - - b.Property("LastFileAnalysisUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("MangaFile"); - }); - - modelBuilder.Entity("API.Entities.MediaError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MediaError"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxCount") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("PublicationStatusLocked") - .HasColumnType("INTEGER"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYear") - .HasColumnType("INTEGER"); - - b.Property("ReleaseYearLocked") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RelationKind") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("TargetSeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.HasIndex("TargetSeriesId"); - - b.ToTable("SeriesRelation"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndingMonth") - .HasColumnType("INTEGER"); - - b.Property("EndingYear") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("StartingMonth") - .HasColumnType("INTEGER"); - - b.Property("StartingYear") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("ReadingList"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("ReadingListId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.HasIndex("ReadingListId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.ToTable("ReadingListItem"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Details") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId") - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventId1") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ScrobbleEventId1"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleError"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterNumber") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .HasColumnType("INTEGER"); - - b.Property("IsProcessed") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("ProcessDateUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("ReviewBody") - .HasColumnType("TEXT"); - - b.Property("ReviewTitle") - .HasColumnType("TEXT"); - - b.Property("ScrobbleEventType") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeNumber") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - - b.Property("LastChapterAdded") - .HasColumnType("TEXT"); - - b.Property("LastChapterAddedUtc") - .HasColumnType("TEXT"); - - b.Property("LastFolderScanned") - .HasColumnType("TEXT"); - - b.Property("LastFolderScannedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("LocalizedName") - .HasColumnType("TEXT"); - - b.Property("LocalizedNameLocked") - .HasColumnType("INTEGER"); - - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedLocalizedName") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("OriginalName") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .HasColumnType("TEXT"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SiteTheme"); - }); - - modelBuilder.Entity("API.Entities.Tag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedTitle") - .IsUnique(); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.Property("AppUsersId") - .HasColumnType("INTEGER"); - - b.Property("LibrariesId") - .HasColumnType("INTEGER"); - - b.HasKey("AppUsersId", "LibrariesId"); - - b.HasIndex("LibrariesId"); - - b.ToTable("AppUserLibrary"); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "GenresId"); - - b.HasIndex("GenresId"); - - b.ToTable("ChapterGenre"); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("SeriesMetadatasId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("SeriesMetadataTag"); - }); - - modelBuilder.Entity("API.Entities.AppUserBookmark", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("AppUserLibrary", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("AppUsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", null) - .WithMany() - .HasForeignKey("LibrariesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterGenre", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("SeriesMetadataTag", b => - { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Navigation("Bookmarks"); - - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs deleted file mode 100644 index 4800cf3fa..000000000 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class LibraryRemoveSortPrefix : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "RemovePrefixForSortName", - table: "Library", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "RemovePrefixForSortName", - table: "Library"); - } - } -} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 62d1fb1ef..65e12c0a8 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1,8 +1,6 @@ // using System; -using System.Collections.Generic; using API.Data; -using API.Entities.MetadataMatching; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -17,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -87,9 +85,6 @@ namespace API.Data.Migrations b.Property("EmailConfirmed") .HasColumnType("INTEGER"); - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - b.Property("LastActive") .HasColumnType("TEXT"); @@ -102,12 +97,6 @@ namespace API.Data.Migrations b.Property("LockoutEnd") .HasColumnType("TEXT"); - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - b.Property("NormalizedEmail") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -129,9 +118,6 @@ namespace API.Data.Migrations .IsConcurrencyToken() .HasColumnType("INTEGER"); - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - b.Property("SecurityStamp") .HasColumnType("TEXT"); @@ -197,113 +183,6 @@ namespace API.Data.Migrations b.ToTable("AppUserBookmark"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => { b.Property("Id") @@ -396,16 +275,6 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - b.Property("AppUserId") .HasColumnType("INTEGER"); @@ -480,15 +349,6 @@ namespace API.Data.Migrations b.Property("PageSplitOption") .HasColumnType("INTEGER"); - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - b.Property("PromptForDownloadSize") .HasColumnType("INTEGER"); @@ -513,11 +373,6 @@ namespace API.Data.Migrations b.Property("ThemeId") .HasColumnType("INTEGER"); - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - b.HasKey("Id"); b.HasIndex("AppUserId") @@ -611,123 +466,6 @@ namespace API.Data.Migrations b.ToTable("AppUserRating"); }); - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - modelBuilder.Entity("API.Entities.AppUserRole", b => { b.Property("UserId") @@ -864,27 +602,6 @@ namespace API.Data.Migrations b.ToTable("AppUserTableOfContent"); }); - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - modelBuilder.Entity("API.Entities.Chapter", b => { b.Property("Id") @@ -894,9 +611,6 @@ namespace API.Data.Migrations b.Property("AgeRating") .HasColumnType("INTEGER"); - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - b.Property("AlternateCount") .HasColumnType("INTEGER"); @@ -906,24 +620,12 @@ namespace API.Data.Migrations b.Property("AlternateSeries") .HasColumnType("TEXT"); - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") + b.Property("AvgHoursToRead") .HasColumnType("INTEGER"); b.Property("Count") .HasColumnType("INTEGER"); - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - b.Property("CoverImage") .HasColumnType("TEXT"); @@ -936,100 +638,44 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - b.Property("ISBN") .ValueGeneratedOnAdd() .HasColumnType("TEXT") .HasDefaultValue(""); - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - b.Property("IsSpecial") .HasColumnType("INTEGER"); - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - b.Property("Language") .HasColumnType("TEXT"); - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - b.Property("LastModified") .HasColumnType("TEXT"); b.Property("LastModifiedUtc") .HasColumnType("TEXT"); - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); - b.Property("MaxNumber") - .HasColumnType("REAL"); - b.Property("MinHoursToRead") .HasColumnType("INTEGER"); - b.Property("MinNumber") - .HasColumnType("REAL"); - b.Property("Number") .HasColumnType("TEXT"); b.Property("Pages") .HasColumnType("INTEGER"); - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - b.Property("Range") .HasColumnType("TEXT"); b.Property("ReleaseDate") .HasColumnType("TEXT"); - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - b.Property("SeriesGroup") .HasColumnType("TEXT"); - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - b.Property("StoryArc") .HasColumnType("TEXT"); @@ -1039,30 +685,15 @@ namespace API.Data.Migrations b.Property("Summary") .HasColumnType("TEXT"); - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - b.Property("Title") .HasColumnType("TEXT"); b.Property("TitleName") .HasColumnType("TEXT"); - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - b.Property("TotalCount") .HasColumnType("INTEGER"); - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - b.Property("VolumeId") .HasColumnType("INTEGER"); @@ -1074,9 +705,6 @@ namespace API.Data.Migrations b.Property("WordCount") .HasColumnType("INTEGER"); - b.Property("WriterLocked") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("VolumeId"); @@ -1165,57 +793,6 @@ namespace API.Data.Migrations b.ToTable("Device"); }); - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - modelBuilder.Entity("API.Entities.FolderPath", b => { b.Property("Id") @@ -1258,37 +835,12 @@ namespace API.Data.Migrations b.ToTable("Genre"); }); - modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - modelBuilder.Entity("API.Entities.Library", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - b.Property("AllowScrobbling") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -1303,11 +855,6 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - b.Property("FolderWatching") .HasColumnType("INTEGER"); @@ -1338,15 +885,6 @@ namespace API.Data.Migrations b.Property("Name") .HasColumnType("TEXT"); - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("RemovePrefixForSortName") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - b.Property("Type") .HasColumnType("INTEGER"); @@ -1355,44 +893,6 @@ namespace API.Data.Migrations b.ToTable("Library"); }); - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.Property("Id") @@ -1414,18 +914,12 @@ namespace API.Data.Migrations b.Property("Extension") .HasColumnType("TEXT"); - b.Property("FileName") - .HasColumnType("TEXT"); - b.Property("FilePath") .HasColumnType("TEXT"); b.Property("Format") .HasColumnType("INTEGER"); - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - b.Property("LastFileAnalysis") .HasColumnType("TEXT"); @@ -1483,183 +977,6 @@ namespace API.Data.Migrations b.ToTable("MediaError"); }); - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => { b.Property("Id") @@ -1687,17 +1004,9 @@ namespace API.Data.Migrations b.Property("GenresLocked") .HasColumnType("INTEGER"); - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - b.Property("InkerLocked") .HasColumnType("INTEGER"); - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - b.Property("Language") .HasColumnType("TEXT"); @@ -1707,9 +1016,6 @@ namespace API.Data.Migrations b.Property("LettererLocked") .HasColumnType("INTEGER"); - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - b.Property("MaxCount") .HasColumnType("INTEGER"); @@ -1747,9 +1053,6 @@ namespace API.Data.Migrations b.Property("TagsLocked") .HasColumnType("INTEGER"); - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - b.Property("TotalCount") .HasColumnType("INTEGER"); @@ -1799,231 +1102,26 @@ namespace API.Data.Migrations b.ToTable("SeriesRelation"); }); - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + modelBuilder.Entity("API.Entities.Person", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - b.Property("Name") .HasColumnType("TEXT"); b.Property("NormalizedName") .HasColumnType("TEXT"); - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); + b.Property("Role") + .HasColumnType("INTEGER"); b.HasKey("Id"); b.ToTable("Person"); }); - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - modelBuilder.Entity("API.Entities.ReadingList", b => { b.Property("Id") @@ -2064,15 +1162,9 @@ namespace API.Data.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - b.Property("Promoted") .HasColumnType("INTEGER"); - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - b.Property("StartingMonth") .HasColumnType("INTEGER"); @@ -2193,15 +1285,9 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("ErrorDetails") - .HasColumnType("TEXT"); - b.Property("Format") .HasColumnType("INTEGER"); - b.Property("IsErrored") - .HasColumnType("INTEGER"); - b.Property("IsProcessed") .HasColumnType("INTEGER"); @@ -2235,8 +1321,8 @@ namespace API.Data.Migrations b.Property("SeriesId") .HasColumnType("INTEGER"); - b.Property("VolumeNumber") - .HasColumnType("REAL"); + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -2288,8 +1374,11 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); b.Property("CoverImage") .HasColumnType("TEXT"); @@ -2303,18 +1392,12 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("DontMatch") - .HasColumnType("INTEGER"); - b.Property("FolderPath") .HasColumnType("TEXT"); b.Property("Format") .HasColumnType("INTEGER"); - b.Property("IsBlacklisted") - .HasColumnType("INTEGER"); - b.Property("LastChapterAdded") .HasColumnType("TEXT"); @@ -2342,9 +1425,6 @@ namespace API.Data.Migrations b.Property("LocalizedNameLocked") .HasColumnType("INTEGER"); - b.Property("LowestFolderPath") - .HasColumnType("TEXT"); - b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); @@ -2366,12 +1446,6 @@ namespace API.Data.Migrations b.Property("Pages") .HasColumnType("INTEGER"); - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - b.Property("SortName") .HasColumnType("TEXT"); @@ -2383,6 +1457,8 @@ namespace API.Data.Migrations b.HasKey("Id"); + b.HasIndex("AppUserId"); + b.HasIndex("LibraryId"); b.ToTable("Series"); @@ -2449,27 +1525,15 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - b.Property("Created") .HasColumnType("TEXT"); b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("Description") - .HasColumnType("TEXT"); - b.Property("FileName") .HasColumnType("TEXT"); - b.Property("GitHubPath") - .HasColumnType("TEXT"); - b.Property("IsDefault") .HasColumnType("INTEGER"); @@ -2485,15 +1549,9 @@ namespace API.Data.Migrations b.Property("NormalizedName") .HasColumnType("TEXT"); - b.Property("PreviewUrls") - .HasColumnType("TEXT"); - b.Property("Provider") .HasColumnType("INTEGER"); - b.Property("ShaHash") - .HasColumnType("TEXT"); - b.HasKey("Id"); b.ToTable("SiteTheme"); @@ -2525,15 +1583,12 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); b.Property("CoverImage") .HasColumnType("TEXT"); - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - b.Property("Created") .HasColumnType("TEXT"); @@ -2546,21 +1601,12 @@ namespace API.Data.Migrations b.Property("LastModifiedUtc") .HasColumnType("TEXT"); - b.Property("LookupName") - .HasColumnType("TEXT"); - b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); - b.Property("MaxNumber") - .HasColumnType("REAL"); - b.Property("MinHoursToRead") .HasColumnType("INTEGER"); - b.Property("MinNumber") - .HasColumnType("REAL"); - b.Property("Name") .HasColumnType("TEXT"); @@ -2570,12 +1616,6 @@ namespace API.Data.Migrations b.Property("Pages") .HasColumnType("INTEGER"); - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - b.Property("SeriesId") .HasColumnType("INTEGER"); @@ -2589,21 +1629,6 @@ namespace API.Data.Migrations b.ToTable("Volume"); }); - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - modelBuilder.Entity("AppUserLibrary", b => { b.Property("AppUsersId") @@ -2634,6 +1659,21 @@ namespace API.Data.Migrations b.ToTable("ChapterGenre"); }); + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + modelBuilder.Entity("ChapterTag", b => { b.Property("ChaptersId") @@ -2664,51 +1704,6 @@ namespace API.Data.Migrations b.ToTable("CollectionTagSeriesMetadata"); }); - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - modelBuilder.Entity("GenreSeriesMetadata", b => { b.Property("GenresId") @@ -2808,6 +1803,21 @@ namespace API.Data.Migrations b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + modelBuilder.Entity("SeriesMetadataTag", b => { b.Property("SeriesMetadatasId") @@ -2834,44 +1844,6 @@ namespace API.Data.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -2978,17 +1950,6 @@ namespace API.Data.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - modelBuilder.Entity("API.Entities.AppUserRole", b => { b.HasOne("API.Entities.AppRole", "Role") @@ -3063,25 +2024,6 @@ namespace API.Data.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Chapter", b => { b.HasOne("API.Entities.Volume", "Volume") @@ -3104,17 +2046,6 @@ namespace API.Data.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - modelBuilder.Entity("API.Entities.FolderPath", b => { b.HasOne("API.Entities.Library", "Library") @@ -3126,28 +2057,6 @@ namespace API.Data.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - modelBuilder.Entity("API.Entities.MangaFile", b => { b.HasOne("API.Entities.Chapter", "Chapter") @@ -3159,42 +2068,6 @@ namespace API.Data.Migrations b.Navigation("Chapter"); }); - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => { b.HasOne("API.Entities.Series", "Series") @@ -3225,66 +2098,6 @@ namespace API.Data.Migrations b.Navigation("TargetSeries"); }); - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - modelBuilder.Entity("API.Entities.ReadingList", b => { b.HasOne("API.Entities.AppUser", "AppUser") @@ -3396,6 +2209,10 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Series", b => { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + b.HasOne("API.Entities.Library", "Library") .WithMany("Series") .HasForeignKey("LibraryId") @@ -3416,21 +2233,6 @@ namespace API.Data.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("AppUserLibrary", b => { b.HasOne("API.Entities.AppUser", null) @@ -3461,6 +2263,21 @@ namespace API.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("ChapterTag", b => { b.HasOne("API.Entities.Chapter", null) @@ -3491,51 +2308,6 @@ namespace API.Data.Migrations .IsRequired(); }); - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("GenreSeriesMetadata", b => { b.HasOne("API.Entities.Genre", null) @@ -3587,6 +2359,21 @@ namespace API.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("SeriesMetadataTag", b => { b.HasOne("API.Entities.Metadata.SeriesMetadata", null) @@ -3611,10 +2398,6 @@ namespace API.Data.Migrations { b.Navigation("Bookmarks"); - b.Navigation("ChapterRatings"); - - b.Navigation("Collections"); - b.Navigation("DashboardStreams"); b.Navigation("Devices"); @@ -3627,8 +2410,6 @@ namespace API.Data.Migrations b.Navigation("ReadingLists"); - b.Navigation("ReadingProfiles"); - b.Navigation("ScrobbleHolds"); b.Navigation("SideNavStreams"); @@ -3646,16 +2427,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Chapter", b => { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - b.Navigation("Files"); - b.Navigation("People"); - - b.Navigation("Ratings"); - b.Navigation("UserProgress"); }); @@ -3663,32 +2436,9 @@ namespace API.Data.Migrations { b.Navigation("Folders"); - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - modelBuilder.Entity("API.Entities.ReadingList", b => { b.Navigation("Items"); @@ -3696,8 +2446,6 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Series", b => { - b.Navigation("ExternalSeriesMetadata"); - b.Navigation("Metadata"); b.Navigation("Progress"); diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/API/Data/Misc/RecentlyAddedSeries.cs index 1ea5b1d3e..d5cfece45 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/API/Data/Misc/RecentlyAddedSeries.cs @@ -18,6 +18,6 @@ public class RecentlyAddedSeries public string? ChapterRange { get; init; } public string? ChapterTitle { get; init; } public bool IsSpecial { get; init; } - public float VolumeNumber { get; init; } + public int VolumeNumber { get; init; } public AgeRating AgeRating { get; init; } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index a672259ad..ec3af56a7 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using API.Data.ManualMigrations; using API.DTOs; -using API.DTOs.Progress; using API.Entities; using API.Entities.Enums; -using API.Extensions.QueryExtensions; using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -16,11 +13,9 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable - public interface IAppUserProgressRepository { void Update(AppUserProgress userProgress); - void Remove(AppUserProgress userProgress); Task CleanupAbandonedChapters(); Task UserHasProgress(LibraryType libraryType, int userId); Task GetUserProgressAsync(int chapterId, int userId); @@ -36,13 +31,11 @@ public interface IAppUserProgressRepository Task GetUserProgressDtoAsync(int chapterId, int userId); Task AnyUserProgressForSeriesAsync(int seriesId, int userId); Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); - Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); + Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); Task GetLatestProgressForSeries(int seriesId, int userId); Task GetFirstProgressForSeries(int seriesId, int userId); - Task UpdateAllProgressThatAreMoreThanChapterPages(); - Task> GetUserProgressForChapter(int chapterId, int userId = 0); } - +#nullable disable public class AppUserProgressRepository : IAppUserProgressRepository { private readonly DataContext _context; @@ -59,11 +52,6 @@ public class AppUserProgressRepository : IAppUserProgressRepository _context.Entry(userProgress).State = EntityState.Modified; } - public void Remove(AppUserProgress userProgress) - { - _context.Remove(userProgress); - } - /// /// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well. /// @@ -177,23 +165,20 @@ public class AppUserProgressRepository : IAppUserProgressRepository (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) - .Select(p => p.chapter.MaxNumber) + .Select(p => p.chapter.Range) .ToListAsync(); - return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d); + return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d))); } - public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) { var list = await _context.AppUserProgresses .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) - .Select(p => p.chapter.Volume.MaxNumber) + .Select(p => p.chapter.Volume.Number) .ToListAsync(); - return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } @@ -213,64 +198,6 @@ public class AppUserProgressRepository : IAppUserProgressRepository return list.Count == 0 ? null : list.DefaultIfEmpty().Min(); } - public async Task UpdateAllProgressThatAreMoreThanChapterPages() - { - var updates = _context.AppUserProgresses - .Join(_context.Chapter, - progress => progress.ChapterId, - chapter => chapter.Id, - (progress, chapter) => new - { - Progress = progress, - Chapter = chapter - }) - .Where(joinResult => joinResult.Progress.PagesRead > joinResult.Chapter.Pages) - .Select(result => new - { - ProgressId = result.Progress.Id, - NewPagesRead = Math.Min(result.Progress.PagesRead, result.Chapter.Pages) - }) - .AsEnumerable(); - - // Need to run this Raw because DataContext will update LastModified on the entity which breaks ordering for progress - var sqlBuilder = new StringBuilder(); - foreach (var update in updates) - { - sqlBuilder.Append($"UPDATE AppUserProgresses SET PagesRead = {update.NewPagesRead} WHERE Id = {update.ProgressId};"); - } - - // Execute the batch SQL - var batchSql = sqlBuilder.ToString(); - await _context.Database.ExecuteSqlRawAsync(batchSql); - } - - /// - /// - /// - /// - /// If 0, will pull all records - /// - public async Task> GetUserProgressForChapter(int chapterId, int userId = 0) - { - return await _context.AppUserProgresses - .WhereIf(userId > 0, p => p.AppUserId == userId) - .Where(p => p.ChapterId == chapterId) - .Include(p => p.AppUser) - .Select(p => new FullProgressDto() - { - AppUserId = p.AppUserId, - ChapterId = p.ChapterId, - PagesRead = p.PagesRead, - Id = p.Id, - Created = p.Created, - CreatedUtc = p.CreatedUtc, - LastModified = p.LastModified, - LastModifiedUtc = p.LastModifiedUtc, - UserName = p.AppUser.UserName - }) - .ToListAsync(); - } - #nullable enable public async Task GetUserProgressAsync(int chapterId, int userId) { diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/API/Data/Repositories/AppUserReadingProfileRepository.cs deleted file mode 100644 index 11b97f21a..000000000 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - - -public interface IAppUserReadingProfileRepository -{ - - /// - /// Return the given profile if it belongs the user - /// - /// - /// - /// - Task GetUserProfile(int userId, int profileId); - /// - /// Returns all reading profiles for the user - /// - /// - /// - Task> GetProfilesForUser(int userId, bool skipImplicit = false); - /// - /// Returns all reading profiles for the user - /// - /// - /// - Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false); - /// - /// Is there a user reading profile with this name (normalized) - /// - /// - /// - /// - Task IsProfileNameInUse(int userId, string name); - - void Add(AppUserReadingProfile readingProfile); - void Update(AppUserReadingProfile readingProfile); - void Remove(AppUserReadingProfile readingProfile); - void RemoveRange(IEnumerable readingProfiles); -} - -public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository -{ - public async Task GetUserProfile(int userId, int profileId) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId && rp.Id == profileId) - .FirstOrDefaultAsync(); - } - - public async Task> GetProfilesForUser(int userId, bool skipImplicit = false) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId) - .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ToListAsync(); - } - - /// - /// Returns all Reading Profiles for the User - /// - /// - /// - public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId) - .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task IsProfileNameInUse(int userId, string name) - { - var normalizedName = name.ToNormalized(); - - return await context.AppUserReadingProfiles - .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) - .AnyAsync(); - } - - public void Add(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Add(readingProfile); - } - - public void Update(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Update(readingProfile).State = EntityState.Modified; - } - - public void Remove(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Remove(readingProfile); - } - - public void RemoveRange(IEnumerable readingProfiles) - { - context.AppUserReadingProfiles.RemoveRange(readingProfiles); - } -} diff --git a/API/Data/Repositories/AppUserSmartFilterRepository.cs b/API/Data/Repositories/AppUserSmartFilterRepository.cs index c7f981daa..fd8933634 100644 --- a/API/Data/Repositories/AppUserSmartFilterRepository.cs +++ b/API/Data/Repositories/AppUserSmartFilterRepository.cs @@ -8,7 +8,6 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IAppUserSmartFilterRepository { @@ -56,7 +55,6 @@ public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository public async Task GetById(int smartFilterId) { - return await _context.AppUserSmartFilter - .FirstOrDefaultAsync(d => d.Id == smartFilterId); + return await _context.AppUserSmartFilter.FirstOrDefaultAsync(d => d.Id == smartFilterId); } } diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 27d21df74..106de2386 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -5,10 +5,8 @@ using System.Threading.Tasks; using API.DTOs; using API.DTOs.Metadata; using API.DTOs.Reader; -using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; @@ -16,7 +14,6 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable [Flags] public enum ChapterIncludes @@ -24,18 +21,11 @@ public enum ChapterIncludes None = 1, Volumes = 2, Files = 4, - People = 8, - Genres = 16, - Tags = 32, - ExternalReviews = 1 << 6, - ExternalRatings = 1 << 7 } public interface IChapterRepository { void Update(Chapter chapter); - void Remove(Chapter chapter); - void Remove(IList chapters); Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None); Task GetChapterInfoDtoAsync(int chapterId); Task GetChapterTotalPagesAsync(int chapterId); @@ -43,7 +33,7 @@ public interface IChapterRepository Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task> GetFilesForChapterAsync(int chapterId); - Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None); + Task> GetChaptersAsync(int volumeId); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); Task GetChapterCoverImageAsync(int chapterId); Task> GetAllCoverImagesAsync(); @@ -51,12 +41,6 @@ public interface IChapterRepository Task> GetCoverImagesForLockedChaptersAsync(); Task AddChapterModifiers(int userId, ChapterDto chapter); IEnumerable GetChaptersForSeries(int seriesId); - Task> GetAllChaptersForSeries(int seriesId); - Task GetAverageUserRating(int chapterId, int userId); - Task> GetExternalChapterReviewDtos(int chapterId); - Task> GetExternalChapterReview(int chapterId); - Task> GetExternalChapterRatingDtos(int chapterId); - Task> GetExternalChapterRatings(int chapterId); } public class ChapterRepository : IChapterRepository { @@ -74,16 +58,6 @@ public class ChapterRepository : IChapterRepository _context.Entry(chapter).State = EntityState.Modified; } - public void Remove(Chapter chapter) - { - _context.Chapter.Remove(chapter); - } - - public void Remove(IList chapters) - { - _context.Chapter.RemoveRange(chapters); - } - public async Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None) { return await _context.Chapter @@ -103,7 +77,7 @@ public class ChapterRepository : IChapterRepository .Where(c => c.Id == chapterId) .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new { - ChapterNumber = chapter.MinNumber, + ChapterNumber = chapter.Range, VolumeNumber = volume.Name, VolumeId = volume.Id, chapter.IsSpecial, @@ -127,8 +101,8 @@ public class ChapterRepository : IChapterRepository }) .Select(data => new ChapterInfoDto() { - ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this - VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this + ChapterNumber = data.ChapterNumber, + VolumeNumber = data.VolumeNumber + string.Empty, VolumeId = data.VolumeId, IsSpecial = data.IsSpecial, SeriesId = data.SeriesId, @@ -200,7 +174,6 @@ public class ChapterRepository : IChapterRepository { return await _context.Chapter .Includes(includes) - .OrderBy(c => c.SortOrder) .FirstOrDefaultAsync(c => c.Id == chapterId); } @@ -209,12 +182,10 @@ public class ChapterRepository : IChapterRepository /// /// /// - public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None) + public async Task> GetChaptersAsync(int volumeId) { return await _context.Chapter .Where(c => c.VolumeId == volumeId) - .Includes(includes) - .OrderBy(c => c.SortOrder) .ToListAsync(); } @@ -295,79 +266,11 @@ public class ChapterRepository : IChapterRepository return chapter; } - /// - /// Includes Volumes - /// - /// - /// public IEnumerable GetChaptersForSeries(int seriesId) { return _context.Chapter .Where(c => c.Volume.SeriesId == seriesId) - .OrderBy(c => c.SortOrder) .Include(c => c.Volume) .AsEnumerable(); } - - public async Task> GetAllChaptersForSeries(int seriesId) - { - return await _context.Chapter - .Where(c => c.Volume.SeriesId == seriesId) - .OrderBy(c => c.SortOrder) - .Include(c => c.Volume) - .Include(c => c.People) - .ThenInclude(cp => cp.Person) - .ToListAsync(); - } - - public async Task GetAverageUserRating(int chapterId, int userId) - { - // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserChapterRating - .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); - if (countOfRatingsThatAreUser == 1) - { - return 0; - } - var avg = (await _context.AppUserChapterRating - .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .AverageAsync(r => (int?) r.Rating)); - return avg.HasValue ? (int) (avg.Value * 20) : 0; - } - - public async Task> GetExternalChapterReviewDtos(int chapterId) - { - return await _context.Chapter - .Where(c => c.Id == chapterId) - .SelectMany(c => c.ExternalReviews) - // Don't use ProjectTo, it fails to map int to float (??) - .Select(r => _mapper.Map(r)) - .ToListAsync(); - } - - public async Task> GetExternalChapterReview(int chapterId) - { - return await _context.Chapter - .Where(c => c.Id == chapterId) - .SelectMany(c => c.ExternalReviews) - .ToListAsync(); - } - - public async Task> GetExternalChapterRatingDtos(int chapterId) - { - return await _context.Chapter - .Where(c => c.Id == chapterId) - .SelectMany(c => c.ExternalRatings) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetExternalChapterRatings(int chapterId) - { - return await _context.Chapter - .Where(c => c.Id == chapterId) - .SelectMany(c => c.ExternalRatings) - .ToListAsync(); - } } diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs index 562f59e91..4e35ef613 100644 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ b/API/Data/Repositories/CollectionTagRepository.cs @@ -3,64 +3,41 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data.Misc; -using API.DTOs.Collection; +using API.DTOs.CollectionTags; using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Services.Plus; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable - [Flags] public enum CollectionTagIncludes { None = 1, SeriesMetadata = 2, - SeriesMetadataWithSeries = 4 -} - -[Flags] -public enum CollectionIncludes -{ - None = 1, - Series = 2, } public interface ICollectionTagRepository { - void Remove(AppUserCollection tag); + void Add(CollectionTag tag); + void Remove(CollectionTag tag); + Task> GetAllTagDtosAsync(); + Task> SearchTagDtosAsync(string searchQuery, int userId); Task GetCoverImageAsync(int collectionTagId); - Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None); - void Update(AppUserCollection tag); - Task RemoveCollectionsWithoutSeries(); - - Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None); - /// - /// Returns all of the user's collections with the option of other user's promoted - /// - /// - /// - /// - Task> GetCollectionDtosAsync(int userId, bool includePromoted = false); - Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false); - + Task> GetAllPromotedTagDtosAsync(int userId); + Task GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None); + void Update(CollectionTag tag); + Task RemoveTagsWithoutSeries(); + Task> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None); Task> GetAllCoverImagesAsync(); - Task CollectionExists(string title, int userId); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); + Task TagExists(string title); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task> GetRandomCoverImagesAsync(int collectionId); - Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None); - Task UpdateCollectionAgeRating(AppUserCollection tag); - Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None); - Task> GetAllCollectionsForSyncing(DateTime expirationTime); } - public class CollectionTagRepository : ICollectionTagRepository { private readonly DataContext _context; @@ -72,12 +49,17 @@ public class CollectionTagRepository : ICollectionTagRepository _mapper = mapper; } - public void Remove(AppUserCollection tag) + public void Add(CollectionTag tag) { - _context.AppUserCollection.Remove(tag); + _context.CollectionTag.Add(tag); } - public void Update(AppUserCollection tag) + public void Remove(CollectionTag tag) + { + _context.CollectionTag.Remove(tag); + } + + public void Update(CollectionTag tag) { _context.Entry(tag).State = EntityState.Modified; } @@ -85,53 +67,29 @@ public class CollectionTagRepository : ICollectionTagRepository /// /// Removes any collection tags without any series /// - public async Task RemoveCollectionsWithoutSeries() + public async Task RemoveTagsWithoutSeries() { - var tagsToDelete = await _context.AppUserCollection - .Include(c => c.Items) - .Where(c => c.Items.Count == 0) + var tagsToDelete = await _context.CollectionTag + .Include(c => c.SeriesMetadatas) + .Where(c => c.SeriesMetadatas.Count == 0) .AsSplitQuery() .ToListAsync(); - _context.RemoveRange(tagsToDelete); return await _context.SaveChangesAsync(); } - public async Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None) + public async Task> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None) { - return await _context.AppUserCollection + return await _context.CollectionTag .OrderBy(c => c.NormalizedTitle) .Includes(includes) .ToListAsync(); } - public async Task> GetCollectionDtosAsync(int userId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .Where(uc => uc.Items.Any(s => s.Id == seriesId)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - public async Task GetCoverImageAsync(int collectionTagId) { - return await _context.AppUserCollection + return await _context.CollectionTag .Where(c => c.Id == collectionTagId) .Select(c => c.CoverImage) .SingleOrDefaultAsync(); @@ -139,30 +97,23 @@ public class CollectionTagRepository : ICollectionTagRepository public async Task> GetAllCoverImagesAsync() { - return await _context.AppUserCollection + return (await _context.CollectionTag .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); + .ToListAsync())!; } - /// - /// If any tag exists for that given user's collections - /// - /// - /// - /// - public async Task CollectionExists(string title, int userId) + public async Task TagExists(string title) { var normalized = title.ToNormalized(); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId) + return await _context.CollectionTag .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); - return await _context.AppUserCollection + return await _context.CollectionTag .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .ToListAsync(); } @@ -170,61 +121,44 @@ public class CollectionTagRepository : ICollectionTagRepository public async Task> GetRandomCoverImagesAsync(int collectionId) { var random = new Random(); - var data = await _context.AppUserCollection + var data = await _context.CollectionTag .Where(t => t.Id == collectionId) - .SelectMany(uc => uc.Items.Select(series => series.CoverImage)) + .SelectMany(t => t.SeriesMetadatas) + .Select(sm => sm.Series.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync(); - return data .OrderBy(_ => random.Next()) .Take(4) .ToList(); } - public async Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None) + public async Task> GetAllTagDtosAsync() { - return await _context.AppUserCollection - .Where(c => c.AppUserId == userId) - .Includes(includes) + + return await _context.CollectionTag + .OrderBy(c => c.NormalizedTitle) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task UpdateCollectionAgeRating(AppUserCollection tag) + public async Task> GetAllPromotedTagDtosAsync(int userId) { - var maxAgeRating = await _context.AppUserCollection - .Where(t => t.Id == tag.Id) - .SelectMany(uc => uc.Items.Select(s => s.Metadata)) - .Select(sm => sm.AgeRating) - .ToListAsync(); - - - tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown; - await _context.SaveChangesAsync(); - } - - public async Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => tags.Contains(c.Id)) - .Includes(includes) - .AsSplitQuery() + var userRating = await GetUserAgeRestriction(userId); + return await _context.CollectionTag + .Where(c => c.Promoted) + .RestrictAgainstAgeRestriction(userRating) + .OrderBy(c => c.NormalizedTitle) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task> GetAllCollectionsForSyncing(DateTime expirationTime) - { - return await _context.AppUserCollection - .Where(c => c.Source == ScrobbleProvider.Mal) - .Where(c => c.LastSyncUtc <= expirationTime) - .Include(c => c.Items) - .AsSplitQuery() - .ToListAsync(); - } - public async Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None) + public async Task GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None) { - return await _context.AppUserCollection + return await _context.CollectionTag .Where(c => c.Id == tagId) .Includes(includes) .AsSplitQuery() @@ -244,12 +178,16 @@ public class CollectionTagRepository : ICollectionTagRepository .SingleAsync(); } - public async Task> SearchTagDtosAsync(string searchQuery, int userId) + public async Task> SearchTagDtosAsync(string searchQuery, int userId) { var userRating = await GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Search(searchQuery, userId, userRating) - .ProjectTo(_mapper.ConfigurationProvider) + return await _context.CollectionTag + .Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%") + || EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%")) + .RestrictAgainstAgeRestriction(userRating) + .OrderBy(s => s.NormalizedTitle) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } } diff --git a/API/Data/Repositories/CoverDbRepository.cs b/API/Data/Repositories/CoverDbRepository.cs deleted file mode 100644 index ed13493ab..000000000 --- a/API/Data/Repositories/CoverDbRepository.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using API.DTOs.CoverDb; -using API.Entities; -using API.Entities.Person; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace API.Data.Repositories; -#nullable enable - -/// -/// This is a manual repository, not a DB repo -/// -public class CoverDbRepository -{ - private readonly List _authors; - - public CoverDbRepository(string filePath) - { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - // Read and deserialize YAML file - var yamlContent = File.ReadAllText(filePath); - var peopleData = deserializer.Deserialize(yamlContent); - _authors = peopleData.People; - } - - public CoverDbAuthor? FindAuthorByNameOrAlias(string name) - { - return _authors.Find(author => - author.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || - author.Aliases.Contains(name, StringComparer.OrdinalIgnoreCase)); - } - - public CoverDbAuthor? FindBestAuthorMatch(Person person) - { - var aniListId = person.AniListId > 0 ? $"{person.AniListId}" : string.Empty; - var highestScore = 0; - CoverDbAuthor? bestMatch = null; - - foreach (var author in _authors) - { - var score = 0; - - // Check metadata IDs and add points if they match - if (!string.IsNullOrEmpty(author.Ids.AmazonId) && author.Ids.AmazonId == person.Asin) - { - score += 10; - } - if (!string.IsNullOrEmpty(author.Ids.AnilistId) && author.Ids.AnilistId == aniListId) - { - score += 10; - } - if (!string.IsNullOrEmpty(author.Ids.HardcoverId) && author.Ids.HardcoverId == person.HardcoverId) - { - score += 10; - } - - // Check for exact name match - if (author.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase)) - { - score += 7; - } - - // Check for alias match - if (author.Aliases.Contains(person.Name, StringComparer.OrdinalIgnoreCase)) - { - score += 5; - } - - // Update the best match if current score is higher - if (score <= highestScore) continue; - - highestScore = score; - bestMatch = author; - } - - return bestMatch; - } - -} diff --git a/API/Data/Repositories/EmailHistoryRepository.cs b/API/Data/Repositories/EmailHistoryRepository.cs deleted file mode 100644 index e5ed1377a..000000000 --- a/API/Data/Repositories/EmailHistoryRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Email; -using API.Entities; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -public interface IEmailHistoryRepository -{ - Task> GetEmailDtos(UserParams userParams); -} - -public class EmailHistoryRepository : IEmailHistoryRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public EmailHistoryRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - - public async Task> GetEmailDtos(UserParams userParams) - { - return await _context.EmailHistory - .OrderByDescending(h => h.SendDate) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs deleted file mode 100644 index 377344a3c..000000000 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Services.Plus; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IExternalSeriesMetadataRepository -{ - void Attach(ExternalSeriesMetadata metadata); - void Attach(ExternalRating rating); - void Attach(ExternalReview review); - void Remove(IEnumerable? reviews); - void Remove(IEnumerable? ratings); - void Remove(IEnumerable? recommendations); - void Remove(ExternalSeriesMetadata metadata); - Task GetExternalSeriesMetadata(int seriesId); - Task NeedsDataRefresh(int seriesId); - Task GetSeriesDetailPlusDto(int seriesId); - Task LinkRecommendationsToSeries(Series series); - Task IsBlacklistedSeries(int seriesId); - Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false); - Task> GetAllSeries(ManageMatchFilterDto filter); -} - -public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(ExternalSeriesMetadata metadata) - { - _context.ExternalSeriesMetadata.Attach(metadata); - } - - public void Attach(ExternalRating rating) - { - _context.ExternalRating.Attach(rating); - } - - public void Attach(ExternalReview review) - { - _context.ExternalReview.Attach(review); - } - - public void Remove(IEnumerable? reviews) - { - if (reviews == null) return; - _context.ExternalReview.RemoveRange(reviews); - } - - public void Remove(IEnumerable? ratings) - { - if (ratings == null) return; - _context.ExternalRating.RemoveRange(ratings); - } - - public void Remove(IEnumerable? recommendations) - { - if (recommendations == null) return; - _context.ExternalRecommendation.RemoveRange(recommendations); - } - - public void Remove(ExternalSeriesMetadata? metadata) - { - if (metadata == null) return; - _context.ExternalSeriesMetadata.Remove(metadata); - } - - /// - /// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables - /// - /// - /// - public Task GetExternalSeriesMetadata(int seriesId) - { - return _context.ExternalSeriesMetadata - .Where(s => s.SeriesId == seriesId) - .Include(s => s.ExternalReviews) - .Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore)) - .Include(s => s.ExternalRecommendations.OrderBy(r => r.Id)) - .AsSplitQuery() - .FirstOrDefaultAsync(); - } - - public async Task NeedsDataRefresh(int seriesId) - { - // TODO: Add unit test - var row = await _context.ExternalSeriesMetadata - .Where(s => s.SeriesId == seriesId) - .FirstOrDefaultAsync(); - - return row == null || row.ValidUntilUtc <= DateTime.UtcNow; - } - - public async Task GetSeriesDetailPlusDto(int seriesId) - { - // TODO: Add unit test - var seriesDetailDto = await _context.ExternalSeriesMetadata - .Where(m => m.SeriesId == seriesId) - .Include(m => m.ExternalRatings) - .Include(m => m.ExternalReviews) - .Include(m => m.ExternalRecommendations) - .FirstOrDefaultAsync(); - - if (seriesDetailDto == null) - { - return null; // or handle the case when seriesDetailDto is not found - } - - var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations - .Where(r => r.SeriesId == null) - .Select(r => _mapper.Map(r)) - .ToList(); - - var ownedIds = seriesDetailDto.ExternalRecommendations - .Where(r => r.SeriesId != null) - .Select(r => r.SeriesId) - .ToList(); - - var ownedSeriesRecommendations = await _context.Series - .Where(s => ownedIds.Contains(s.Id)) - .OrderBy(s => s.SortName.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - - IEnumerable reviews = []; - if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) - { - reviews = seriesDetailDto.ExternalReviews - .Select(r => - { - var ret = _mapper.Map(r); - ret.IsExternal = true; - return ret; - }) - .OrderByDescending(r => r.Score); - } - - IEnumerable ratings = []; - if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0) - { - ratings = seriesDetailDto.ExternalRatings - .Select(r => _mapper.Map(r)); - } - - - var seriesDetailPlusDto = new SeriesDetailPlusDto() - { - Ratings = ratings, - Reviews = reviews, - Recommendations = new RecommendationDto() - { - ExternalSeries = externalSeriesRecommendations, - OwnedSeries = ownedSeriesRecommendations - } - }; - - return seriesDetailPlusDto; - } - - /// - /// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name - /// - /// - /// - public async Task LinkRecommendationsToSeries(Series series) - { - var recMatches = await _context.ExternalRecommendation - .Where(r => r.SeriesId == null || r.SeriesId == 0) - .Where(r => EF.Functions.Like(r.Name, series.Name) || - EF.Functions.Like(r.Name, series.LocalizedName)) - .ToListAsync(); - - foreach (var rec in recMatches) - { - rec.SeriesId = series.Id; - } - - await _context.SaveChangesAsync(); - } - - public Task IsBlacklistedSeries(int seriesId) - { - return _context.Series - .Where(s => s.Id == seriesId) - .Select(s => s.IsBlacklisted) - .FirstOrDefaultAsync(); - } - - - public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false) - { - return await _context.Series - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) - .Where(s => s.Library.AllowMetadataMatching) - .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) - .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0) - .Where(s => !s.IsBlacklisted && !s.DontMatch) - .OrderByDescending(s => s.Library.Type) - .ThenBy(s => s.NormalizedName) - .Select(s => s.Id) - .Take(limit) - .ToListAsync(); - } - - public async Task> GetAllSeries(ManageMatchFilterDto filter) - { - return await _context.Series - .Include(s => s.Library) - .Include(s => s.ExternalSeriesMetadata) - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) - .Where(s => s.Library.AllowMetadataMatching) - .WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType) - .FilterMatchState(filter.MatchStateOption) - .OrderBy(s => s.NormalizedName) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index d3baa4de6..b552093e7 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -1,20 +1,15 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IGenreRepository { @@ -22,14 +17,10 @@ public interface IGenreRepository void Remove(Genre genre); Task FindByNameAsync(string genreName); Task> GetAllGenresAsync(); - Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames); + Task> GetAllGenreDtosAsync(int userId); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); - Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None); + Task> GetAllGenreDtosForLibrariesAsync(IList libraryIds, int userId); Task GetCountAsync(); - Task GetRandomGenre(); - Task GetGenreById(int id); - Task> GetAllGenresNotInListAsync(ICollection genreNames); - Task> GetBrowseableGenre(int userId, UserParams userParams); } public class GenreRepository : IGenreRepository @@ -74,63 +65,17 @@ public class GenreRepository : IGenreRepository await _context.SaveChangesAsync(); } - public async Task GetCountAsync() - { - return await _context.Genre.CountAsync(); - } - - public async Task GetRandomGenre() - { - var genreCount = await GetCountAsync(); - if (genreCount == 0) return null; - - var randomIndex = new Random().Next(0, genreCount); - return await _context.Genre - .Skip(randomIndex) - .Take(1) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task GetGenreById(int id) - { - return await _context.Genre - .Where(g => g.Id == id) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task> GetAllGenresAsync() - { - return await _context.Genre.ToListAsync(); - } - - public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames) - { - return await _context.Genre - .Where(g => normalizedNames.Contains(g.NormalizedTitle)) - .ToListAsync(); - } - /// - /// Returns a set of Genre tags for a set of library Ids. - /// AppUserId will restrict returned Genres based on user's age restriction and library access. + /// Returns a set of Genre tags for a set of library Ids. UserId will restrict returned Genres based on user's age restriction. /// - /// /// + /// /// - public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None) + public async Task> GetAllGenreDtosForLibrariesAsync(IList libraryIds, int userId) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId, context).ToListAsync(); - - if (libraryIds is {Count: > 0}) - { - userLibs = userLibs.Where(libraryIds.Contains).ToList(); - } - return await _context.Series - .Where(s => userLibs.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Genres) .AsSplitQuery() @@ -140,66 +85,25 @@ public class GenreRepository : IGenreRepository .ToListAsync(); } - /// - /// Gets all genres that are not already present in the system. - /// Normalizes genres for lookup, but returns non-normalized names for creation. - /// - /// The list of genre names (non-normalized). - /// A list of genre names that do not exist in the system. - public async Task> GetAllGenresNotInListAsync(ICollection genreNames) + + public async Task GetCountAsync() { - // Group the genres by their normalized names, keeping track of the original names - var normalizedToOriginalMap = genreNames - .Distinct() - .GroupBy(Parser.Normalize) - .ToDictionary(group => group.Key, group => group.First()); // Take the first original name for each normalized name - - var normalizedGenreNames = normalizedToOriginalMap.Keys.ToList(); - - // Query the database for existing genres using the normalized names - var existingGenres = await _context.Genre - .Where(g => normalizedGenreNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field - .Select(g => g.NormalizedTitle) - .ToListAsync(); - - // Find the normalized genres that do not exist in the database - var missingGenres = normalizedGenreNames.Except(existingGenres).ToList(); - - // Return the original non-normalized genres for the missing ones - return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); + return await _context.Genre.CountAsync(); } - public async Task> GetBrowseableGenre(int userId, UserParams userParams) + public async Task> GetAllGenresAsync() + { + return await _context.Genre.ToListAsync(); + } + + public async Task> GetAllGenreDtosAsync(int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); - - var query = _context.Genre + return await _context.Genre .RestrictAgainstAgeRestriction(ageRating) - .WhereIf(allLibrariesCount != userLibs.Count, - genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || - genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) - .Select(g => new BrowseGenreDto - { - Id = g.Id, - Title = g.Title, - SeriesCount = g.SeriesMetadatas - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = g.Chapters - .Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - }) - .OrderBy(g => g.Title); - - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + .OrderBy(g => g.NormalizedTitle) + .AsNoTracking() + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); } } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 78022fa9a..2122de616 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -18,7 +18,6 @@ using Kavita.Common.Extensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable [Flags] public enum LibraryIncludes @@ -27,8 +26,7 @@ public enum LibraryIncludes Series = 2, AppUser = 4, Folders = 8, - FileTypes = 16, - ExcludePatterns = 32 + // Ratings = 16 } public interface ILibraryRepository @@ -40,11 +38,10 @@ public interface ILibraryRepository Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None); IEnumerable GetLibraryDtosForUsernameAsync(string userName); - Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true); + Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None); Task> GetLibrariesForUserIdAsync(int userId); IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None); Task GetLibraryTypeAsync(int libraryId); - Task GetLibraryTypeBySeriesIdAsync(int seriesId); Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None); Task GetTotalFiles(); IEnumerable GetJumpBarAsync(int libraryId); @@ -56,8 +53,6 @@ public interface ILibraryRepository Task> GetAllCoverImagesAsync(); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task GetAllowsScrobblingBySeriesId(int seriesId); - - Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); } public class LibraryRepository : ILibraryRepository @@ -91,9 +86,7 @@ public class LibraryRepository : ILibraryRepository { return _context.Library .Include(l => l.AppUsers) - .Include(l => l.LibraryFileTypes) - .Include(l => l.LibraryExcludePatterns) - .Where(library => library.AppUsers.Any(x => x.UserName!.Equals(userName))) + .Where(library => library.AppUsers.Any(x => x.UserName.Equals(userName))) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -105,16 +98,14 @@ public class LibraryRepository : ILibraryRepository /// /// /// - public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true) + public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None) { var query = _context.Library .Include(l => l.AppUsers) - .Includes(includes) - .AsSplitQuery(); + .Select(l => l); - if (track) return await query.ToListAsync(); - - return await query.AsNoTracking().ToListAsync(); + query = AddIncludesToQuery(query, includes); + return await query.ToListAsync(); } /// @@ -149,20 +140,13 @@ public class LibraryRepository : ILibraryRepository .FirstAsync(); } - public async Task GetLibraryTypeBySeriesIdAsync(int seriesId) - { - return await _context.Series - .Where(s => s.Id == seriesId) - .Select(s => s.Library.Type) - .FirstAsync(); - } - public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) { - return await _context.Library - .Where(x => libraryIds.Contains(x.Id)) - .Includes(includes) - .ToListAsync(); + var query = _context.Library + .Where(x => libraryIds.Contains(x.Id)); + + AddIncludesToQuery(query, includes); + return await query.ToListAsync(); } public async Task GetTotalFiles() @@ -206,7 +190,6 @@ public class LibraryRepository : ILibraryRepository { return await _context.Library .Include(f => f.Folders) - .Include(l => l.LibraryFileTypes) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -218,12 +201,31 @@ public class LibraryRepository : ILibraryRepository { var query = _context.Library - .Where(x => x.Id == libraryId) - .Includes(includes); + .Where(x => x.Id == libraryId); + query = AddIncludesToQuery(query, includes); return await query.SingleOrDefaultAsync(); } + private static IQueryable AddIncludesToQuery(IQueryable query, LibraryIncludes includeFlags) + { + if (includeFlags.HasFlag(LibraryIncludes.Folders)) + { + query = query.Include(l => l.Folders); + } + + if (includeFlags.HasFlag(LibraryIncludes.Series)) + { + query = query.Include(l => l.Series); + } + + if (includeFlags.HasFlag(LibraryIncludes.AppUser)) + { + query = query.Include(l => l.AppUsers); + } + + return query.AsSplitQuery(); + } public async Task LibraryExists(string libraryName) { @@ -261,7 +263,7 @@ public class LibraryRepository : ILibraryRepository public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds) { var ret = await _context.Series - .WhereIf(libraryIds is {Count: > 0} , s => libraryIds!.Contains(s.LibraryId)) + .WhereIf(libraryIds is {Count: > 0} , s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsSplitQuery() .AsNoTracking() @@ -296,7 +298,7 @@ public class LibraryRepository : ILibraryRepository { Title = s, IsoCode = s - }; + };; } public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) @@ -322,7 +324,7 @@ public class LibraryRepository : ILibraryRepository /// public async Task DoAnySeriesFoldersMatch(IEnumerable folders) { - var normalized = folders.Select(Parser.NormalizePath); + var normalized = folders.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath); return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath)); } @@ -357,16 +359,4 @@ public class LibraryRepository : ILibraryRepository .Select(s => s.Library.AllowScrobbling) .SingleOrDefaultAsync(); } - - public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds) - { - return await _context.Series - .Where(series => seriesIds.Contains(series.Id)) - .Select(series => new - { - series.Id, - series.Library.Type - }) - .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); - } } diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs index 89c6bb418..debd52199 100644 --- a/API/Data/Repositories/MangaFileRepository.cs +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -5,13 +5,11 @@ using API.Entities; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IMangaFileRepository { void Update(MangaFile file); Task> GetAllWithMissingExtension(); - Task GetByKoreaderHash(string hash); } public class MangaFileRepository : IMangaFileRepository @@ -34,13 +32,4 @@ public class MangaFileRepository : IMangaFileRepository .Where(f => string.IsNullOrEmpty(f.Extension)) .ToListAsync(); } - - public async Task GetByKoreaderHash(string hash) - { - if (string.IsNullOrEmpty(hash)) return null; - - return await _context.MangaFile - .FirstOrDefaultAsync(f => f.KoreaderHash != null && - f.KoreaderHash.Equals(hash.ToUpper())); - } } diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs index 40501768e..c2e932d32 100644 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ b/API/Data/Repositories/MediaErrorRepository.cs @@ -9,18 +9,15 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IMediaErrorRepository { void Attach(MediaError error); void Remove(MediaError error); - void Remove(IList errors); Task Find(string filename); IEnumerable GetAllErrorDtosAsync(); Task ExistsAsync(MediaError error); Task DeleteAll(); - Task> GetAllErrorsAsync(IList comments); } public class MediaErrorRepository : IMediaErrorRepository @@ -46,11 +43,6 @@ public class MediaErrorRepository : IMediaErrorRepository _context.MediaError.Remove(error); } - public void Remove(IList errors) - { - _context.MediaError.RemoveRange(errors); - } - public Task Find(string filename) { return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync(); @@ -78,11 +70,4 @@ public class MediaErrorRepository : IMediaErrorRepository _context.MediaError.RemoveRange(await _context.MediaError.ToListAsync()); await _context.SaveChangesAsync(); } - - public Task> GetAllErrorsAsync(IList comments) - { - return _context.MediaError - .Where(m => comments.Contains(m.Comment)) - .ToListAsync(); - } } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 76ae94735..146479740 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -1,82 +1,28 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; +using API.Entities; using API.Entities.Enums; -using API.Entities.Person; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable - -[Flags] -public enum PersonIncludes -{ - None = 1 << 0, - Aliases = 1 << 1, - ChapterPeople = 1 << 2, - SeriesPeople = 1 << 3, - - All = Aliases | ChapterPeople | SeriesPeople, -} public interface IPersonRepository { void Attach(Person person); - void Attach(IEnumerable person); void Remove(Person person); - void Remove(ChapterPeople person); - void Remove(SeriesMetadataPeople person); - void Update(Person person); - - Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases); - Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None); - Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None); + Task> GetAllPeople(); + Task> GetAllPersonDtosAsync(int userId); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None); + Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId); + Task GetCountAsync(); - Task GetCoverImageAsync(int personId); - Task GetCoverImageByNameAsync(string name); - Task> GetRolesForPersonByName(int personId, int userId); - Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams); - Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); - Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); - /// - /// Returns a person matched on normalized name or alias - /// - /// - /// - /// - Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); - Task IsNameUnique(string name); - - Task> GetSeriesKnownFor(int personId, int userId); - Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); - /// - /// Returns all people with a matching name, or alias - /// - /// - /// - /// - Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases); - Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases); - - Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases); - - Task AnyAliasExist(string alias); } public class PersonRepository : IPersonRepository @@ -95,37 +41,17 @@ public class PersonRepository : IPersonRepository _context.Person.Attach(person); } - public void Attach(IEnumerable person) - { - _context.Person.AttachRange(person); - } - public void Remove(Person person) { _context.Person.Remove(person); } - public void Remove(ChapterPeople person) - { - _context.ChapterPeople.Remove(person); - } - - public void Remove(SeriesMetadataPeople person) - { - _context.SeriesMetadataPeople.Remove(person); - } - - public void Update(Person person) - { - _context.Person.Update(person); - } - public async Task RemoveAllPeopleNoLongerAssociated() { var peopleWithNoConnections = await _context.Person - .Include(p => p.SeriesMetadataPeople) - .Include(p => p.ChapterPeople) - .Where(p => p.SeriesMetadataPeople.Count == 0 && p.ChapterPeople.Count == 0) + .Include(p => p.SeriesMetadatas) + .Include(p => p.ChapterMetadatas) + .Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0) .AsSplitQuery() .ToListAsync(); @@ -134,22 +60,13 @@ public class PersonRepository : IPersonRepository await _context.SaveChangesAsync(); } - - public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - if (libraryIds is {Count: > 0}) - { - userLibs = userLibs.Where(libraryIds.Contains).ToList(); - } - return await _context.Series - .Where(s => userLibs.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) - .SelectMany(s => s.Metadata.People.Select(p => p.Person)) - .Includes(includes) + .SelectMany(s => s.Metadata.People) .Distinct() .OrderBy(p => p.Name) .AsNoTracking() @@ -158,281 +75,36 @@ public class PersonRepository : IPersonRepository .ToListAsync(); } + public async Task GetCountAsync() + { + return await _context.Person.CountAsync(); + } - public async Task GetCoverImageAsync(int personId) + + public async Task> GetAllPeople() { return await _context.Person - .Where(c => c.Id == personId) - .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); - } - - public async Task GetCoverImageByNameAsync(string name) - { - var normalized = name.ToNormalized(); - return await _context.Person - .Where(c => c.NormalizedName == normalized) - .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); - } - - public async Task> GetRolesForPersonByName(int personId, int userId) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); - - // Query roles from ChapterPeople - var chapterRoles = await _context.Person - .Where(p => p.Id == personId) - .SelectMany(p => p.ChapterPeople) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .Select(cp => cp.Role) - .Distinct() - .ToListAsync(); - - // Query roles from SeriesMetadataPeople - var seriesRoles = await _context.Person - .Where(p => p.Id == personId) - .SelectMany(p => p.SeriesMetadataPeople) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .Select(smp => smp.Role) - .Distinct() - .ToListAsync(); - - // Combine and return distinct roles - return chapterRoles.Union(seriesRoles).Distinct(); - } - - public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - - var query = await CreateFilteredPersonQueryable(userId, filter, ageRating); - - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); - } - - private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) - { - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); - - var query = _context.Person.AsNoTracking(); - - // Apply filtering based on statements - query = BuildPersonFilterQuery(userId, filter, query); - - // Apply restrictions - query = query.RestrictAgainstAgeRestriction(ageRating) - .WhereIf(allLibrariesCount != userLibs.Count, - person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) || - person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId))); - - // Apply sorting and limiting - var sortedQuery = query.SortBy(filter.SortOptions); - - var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo); - - return limitedQuery.Select(p => new BrowsePersonDto - { - Id = p.Id, - Name = p.Name, - Description = p.Description, - CoverImage = p.CoverImage, - SeriesCount = p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata) - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = p.ChapterPeople - .Select(chp => chp.Chapter) - .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - }); - } - - private static IQueryable BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable query) - { - if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query; - - var queries = filterDto.Statements - .Select(statement => BuildPersonFilterGroup(userId, statement, query)) - .ToList(); - - return filterDto.Combination == FilterCombination.And - ? queries.Aggregate((q1, q2) => q1.Intersect(q2)) - : queries.Aggregate((q1, q2) => q1.Union(q2)); - } - - private static IQueryable BuildPersonFilterGroup(int userId, PersonFilterStatementDto statement, IQueryable query) - { - var value = PersonFilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); - - return statement.Field switch - { - PersonFilterField.Name => query.HasPersonName(true, statement.Comparison, (string)value), - PersonFilterField.Role => query.HasPersonRole(true, statement.Comparison, (IList)value), - PersonFilterField.SeriesCount => query.HasPersonSeriesCount(true, statement.Comparison, (int)value), - PersonFilterField.ChapterCount => query.HasPersonChapterCount(true, statement.Comparison, (int)value), - _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") - }; - } - - private static IQueryable ApplyPersonLimit(IQueryable query, int limit) - { - return limit <= 0 ? query : query.Take(limit); - } - - public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) - { - return await _context.Person.Where(p => p.Id == personId) - .Includes(includes) - .FirstOrDefaultAsync(); - } - - public async Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases) - { - var normalized = name.ToNormalized(); - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); - - return await _context.Person - .Where(p => p.NormalizedName == normalized) - .Includes(includes) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases) - { - var normalized = name.ToNormalized(); - return _context.Person - .Includes(includes) - .Where(p => p.NormalizedName == normalized || p.Aliases.Any(pa => pa.NormalizedAlias == normalized)) - .FirstOrDefaultAsync(); - } - - public async Task IsNameUnique(string name) - { - // Should this use Normalized to check? - return !(await _context.Person - .Includes(PersonIncludes.Aliases) - .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); - } - - public async Task> GetSeriesKnownFor(int personId, int userId) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - return await _context.Person - .Where(p => p.Id == personId) - .SelectMany(p => p.SeriesMetadataPeople) - .Select(smp => smp.SeriesMetadata) - .Select(sm => sm.Series) - .RestrictAgainstAgeRestriction(ageRating) - .Where(s => userLibs.Contains(s.LibraryId)) - .Distinct() - .OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating) - .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); - - return await _context.ChapterPeople - .Where(cp => cp.PersonId == personId && cp.Role == role) - .Select(cp => cp.Chapter) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .OrderBy(ch => ch.SortOrder) - .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases) - { - return await _context.Person - .Includes(includes) - .Where(p => normalizedNames.Contains(p.NormalizedName) || p.Aliases.Any(pa => normalizedNames.Contains(pa.NormalizedAlias))) .OrderBy(p => p.Name) .ToListAsync(); } - public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPersonDtosAsync(int userId) { + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Person - .Where(p => p.AniListId == aniListId) - .Includes(includes) - .FirstOrDefaultAsync(); - } - - public async Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases) - { - searchQuery = searchQuery.ToNormalized(); - - return await _context.Person - .Includes(includes) - .Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%") - || p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%"))) + .OrderBy(p => p.Name) + .RestrictAgainstAgeRestriction(ageRating) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - - public async Task AnyAliasExist(string alias) - { - return await _context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == alias.ToNormalized()); - } - - - public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases) - { - return await _context.Person - .Includes(includes) - .OrderBy(p => p.Name) - .ToListAsync(); - } - - public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); - return await _context.Person - .Includes(includes) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) + .Where(p => p.Role == role) .OrderBy(p => p.Name) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); - - return await _context.Person - .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters - .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .OrderBy(p => p.Name) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 6992b2950..9c3d40011 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.DTOs.Person; +using API.DTOs; using API.DTOs.ReadingLists; using API.Entities; using API.Entities.Enums; @@ -17,7 +16,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable [Flags] public enum ReadingListIncludes @@ -38,8 +36,6 @@ public interface IReadingListRepository Task> GetReadingListItemsByIdAsync(int readingListId); Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted); - Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, - bool includePromoted); void Remove(ReadingListItem item); void Add(ReadingList list); void BulkRemove(IEnumerable items); @@ -49,15 +45,10 @@ public interface IReadingListRepository Task> GetRandomCoverImagesAsync(int readingListId); Task> GetAllCoverImagesAsync(); Task ReadingListExists(string name); - Task ReadingListExistsForUser(string name, int userId); - IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role); - Task GetReadingListAllPeopleAsync(int readingListId); + IEnumerable GetReadingListCharactersAsync(int readingListId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task RemoveReadingListsWithoutSeries(); Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task GetReadingListInfoAsync(int readingListId); } public class ReadingListRepository : IReadingListRepository @@ -91,7 +82,7 @@ public class ReadingListRepository : IReadingListRepository return await _context.ReadingList .Where(c => c.Id == readingListId) .Select(c => c.CoverImage) - .FirstOrDefaultAsync(); + .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() @@ -110,7 +101,6 @@ public class ReadingListRepository : IReadingListRepository .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync(); - return data .OrderBy(_ => random.Next()) .Take(4) @@ -125,97 +115,17 @@ public class ReadingListRepository : IReadingListRepository .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } - public async Task ReadingListExistsForUser(string name, int userId) - { - var normalized = name.ToNormalized(); - return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); - } - - public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role) + public IEnumerable GetReadingListCharactersAsync(int readingListId) { return _context.ReadingListItem .Where(item => item.ReadingListId == readingListId) - .SelectMany(item => item.Chapter.People) - .Where(p => p.Role == role) - .OrderBy(p => p.Person.NormalizedName) - .Select(p => p.Person) + .SelectMany(item => item.Chapter.People.Where(p => p.Role == PersonRole.Character)) + .OrderBy(p => p.NormalizedName) .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); } - public async Task GetReadingListAllPeopleAsync(int readingListId) - { - var allPeople = await _context.ReadingListItem - .Where(item => item.ReadingListId == readingListId) - .SelectMany(item => item.Chapter.People) - .OrderBy(p => p.Person.NormalizedName) - .Select(p => new - { - Role = p.Role, - Person = _mapper.Map(p.Person) - }) - .Distinct() - .ToListAsync(); - - // Create the ReadingListCast object - var cast = new ReadingListCast(); - - // Group people by role and populate the appropriate collections - foreach (var personGroup in allPeople.GroupBy(p => p.Role)) - { - var people = personGroup.Select(pg => pg.Person).ToList(); - - switch (personGroup.Key) - { - case PersonRole.Writer: - cast.Writers = people; - break; - case PersonRole.CoverArtist: - cast.CoverArtists = people; - break; - case PersonRole.Publisher: - cast.Publishers = people; - break; - case PersonRole.Character: - cast.Characters = people; - break; - case PersonRole.Penciller: - cast.Pencillers = people; - break; - case PersonRole.Inker: - cast.Inkers = people; - break; - case PersonRole.Imprint: - cast.Imprints = people; - break; - case PersonRole.Colorist: - cast.Colorists = people; - break; - case PersonRole.Letterer: - cast.Letterers = people; - break; - case PersonRole.Editor: - cast.Editors = people; - break; - case PersonRole.Translator: - cast.Translators = people; - break; - case PersonRole.Team: - cast.Teams = people; - break; - case PersonRole.Location: - cast.Locations = people; - break; - case PersonRole.Other: - break; - } - } - - return cast; - } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); @@ -246,51 +156,6 @@ public class ReadingListRepository : IReadingListRepository .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); } - public async Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items) - { - return await _context.ReadingList - .Where(c => ids.Contains(c.Id)) - .Includes(includes) - .AsSplitQuery() - .ToListAsync(); - } - public async Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items) - { - return await _context.ReadingList - .Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId)) - .Includes(includes) - .AsSplitQuery() - .ToListAsync(); - } - - /// - /// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo - /// - /// - /// - public async Task GetReadingListInfoAsync(int readingListId) - { - // Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub) - var readingList = await _context.ReadingList - .Where(rl => rl.Id == readingListId) - .Include(rl => rl.Items) - .ThenInclude(item => item.Series) - .Include(rl => rl.Items) - .ThenInclude(item => item.Volume) - .Include(rl => rl.Items) - .ThenInclude(item => item.Chapter) - .Select(rl => new ReadingListInfoDto() - { - WordCount = rl.Items.Sum(item => item.Chapter.WordCount), - Pages = rl.Items.Sum(item => item.Chapter.Pages), - IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub), - }) - .FirstOrDefaultAsync(); - - return readingList; - } - - public void Remove(ReadingListItem item) { _context.ReadingListItem.Remove(item); @@ -304,11 +169,10 @@ public class ReadingListRepository : IReadingListRepository public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); + var userAgeRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction; var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) - .RestrictAgainstAgeRestriction(user.GetAgeRestriction()); - + .Where(l => l.AgeRating >= userAgeRating); query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.NormalizedTitle); var finalQuery = query.ProjectTo(_mapper.ConfigurationProvider) @@ -319,10 +183,8 @@ public class ReadingListRepository : IReadingListRepository public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); var query = _context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) - .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.SeriesId == seriesId)) .AsSplitQuery() .OrderBy(l => l.Title) @@ -332,21 +194,6 @@ public class ReadingListRepository : IReadingListRepository return await query.ToListAsync(); } - public async Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted) - { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList - .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) - .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) - .Where(l => l.Items.Any(i => i.ChapterId == chapterId)) - .AsSplitQuery() - .OrderBy(l => l.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking(); - - return await query.ToListAsync(); - } - public async Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None) { return await _context.ReadingList @@ -359,7 +206,13 @@ public class ReadingListRepository : IReadingListRepository public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = _context.Library + .Include(l => l.AppUsers) + .Where(library => library.AppUsers.Any(user => user.Id == userId)) + .AsSplitQuery() + .AsNoTracking() + .Select(library => library.Id) + .ToList(); var items = await _context.ReadingListItem .Where(s => s.ReadingListId == readingListId) @@ -370,9 +223,7 @@ public class ReadingListRepository : IReadingListRepository chapter.ReleaseDate, ReadingListItem = data, ChapterTitleName = chapter.TitleName, - FileSize = chapter.Files.Sum(f => f.Bytes), - chapter.Summary, - chapter.IsSpecial + FileSize = chapter.Files.Sum(f => f.Bytes) }) .Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new @@ -383,8 +234,6 @@ public class ReadingListRepository : IReadingListRepository data.ReleaseDate, data.ChapterTitleName, data.FileSize, - data.Summary, - data.IsSpecial, VolumeId = volume.Id, VolumeNumber = volume.Name, }) @@ -402,8 +251,6 @@ public class ReadingListRepository : IReadingListRepository data.ReleaseDate, data.ChapterTitleName, data.FileSize, - data.Summary, - data.IsSpecial, LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(), LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single() }) @@ -425,9 +272,7 @@ public class ReadingListRepository : IReadingListRepository LibraryType = data.LibraryType, ChapterTitleName = data.ChapterTitleName, LibraryName = data.LibraryName, - FileSize = data.FileSize, - Summary = data.Summary, - IsSpecial = data.IsSpecial + FileSize = data.FileSize }) .Where(o => userLibraries.Contains(o.LibraryId)) .OrderBy(rli => rli.Order) @@ -461,10 +306,8 @@ public class ReadingListRepository : IReadingListRepository public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); return await _context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) - .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index 144a3b88e..bff82c681 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -12,7 +12,6 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IScrobbleRepository { @@ -20,36 +19,16 @@ public interface IScrobbleRepository void Attach(ScrobbleError error); void Remove(ScrobbleEvent evt); void Remove(IEnumerable events); - void Remove(IEnumerable errors); void Update(ScrobbleEvent evt); Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false); Task> GetProcessedEvents(int daysAgo); Task Exists(int userId, int seriesId, ScrobbleEventType eventType); Task> GetScrobbleErrors(); - Task> GetAllScrobbleErrorsForSeries(int seriesId); Task ClearScrobbleErrors(); Task HasErrorForSeries(int seriesId); - /// - /// Get all events for a specific user and type - /// - /// - /// - /// - /// If true, only returned not processed events - /// - Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false); - Task> GetUserEventsForSeries(int userId, int seriesId); - /// - /// Return the events with given ids, when belonging to the passed user - /// - /// - /// - /// - Task> GetUserEvents(int userId, IList scrobbleEventIds); + Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType); + Task> GetUserEvents(int userId); Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); - Task> GetAllEventsForSeries(int seriesId); - Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds); - Task> GetEvents(); } /// @@ -86,11 +65,6 @@ public class ScrobbleRepository : IScrobbleRepository _context.ScrobbleEvent.RemoveRange(events); } - public void Remove(IEnumerable errors) - { - _context.ScrobbleError.RemoveRange(errors); - } - public void Update(ScrobbleEvent evt) { _context.Entry(evt).State = EntityState.Modified; @@ -104,7 +78,6 @@ public class ScrobbleRepository : IScrobbleRepository .Include(s => s.Series) .ThenInclude(s => s.Metadata) .Include(s => s.AppUser) - .ThenInclude(u => u.UserPreferences) .Where(s => s.ScrobbleEventType == type) .Where(s => s.IsProcessed == isProcessed) .AsSplitQuery() @@ -115,11 +88,6 @@ public class ScrobbleRepository : IScrobbleRepository .ToListAsync(); } - /// - /// Returns all processed events that were processed 7 or more days ago - /// - /// - /// public async Task> GetProcessedEvents(int daysAgo) { var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo)); @@ -143,13 +111,6 @@ public class ScrobbleRepository : IScrobbleRepository .ToListAsync(); } - public async Task> GetAllScrobbleErrorsForSeries(int seriesId) - { - return await _context.ScrobbleError - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - public async Task ClearScrobbleErrors() { _context.ScrobbleError.RemoveRange(_context.ScrobbleError); @@ -161,66 +122,33 @@ public class ScrobbleRepository : IScrobbleRepository return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId); } - public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false) + public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType) { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) - .WhereIf(isNotProcessed, e => !e.IsProcessed) - .OrderBy(e => e.LastModifiedUtc) - .FirstOrDefaultAsync(); + return await _context.ScrobbleEvent.FirstOrDefaultAsync(e => + e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); } - - public async Task> GetUserEventsForSeries(int userId, int seriesId) + public async Task> GetUserEvents(int userId) { return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) + .Where(e => e.AppUserId == userId) .Include(e => e.Series) .OrderBy(e => e.LastModifiedUtc) .AsSplitQuery() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - - public async Task> GetUserEvents(int userId, IList scrobbleEventIds) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) - .ToListAsync(); - } - public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) { var query = _context.ScrobbleEvent .Where(e => e.AppUserId == userId) .Include(e => e.Series) + .SortBy(filter.Field, filter.IsDescending) .WhereIf(!string.IsNullOrEmpty(filter.Query), s => EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") ) - .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) - .SortBy(filter.Field, filter.IsDescending) .AsSplitQuery() .ProjectTo(_mapper.ConfigurationProvider); return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize); } - - public async Task> GetAllEventsForSeries(int seriesId) - { - return await _context.ScrobbleEvent - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - - public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds) - { - return await _context.ScrobbleEvent - .Where(e => seriesIds.Contains(e.SeriesId)) - .ToListAsync(); - } - - public async Task> GetEvents() - { - return await _context.ScrobbleEvent - .Include(e => e.AppUser) - .ToListAsync(); - } } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 0c4b8350a..298d63610 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1,24 +1,19 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; +using API.Data.ManualMigrations; using API.Data.Misc; using API.Data.Scanner; using API.DTOs; -using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Dashboard; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.ReadingLists; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; using API.DTOs.Search; using API.DTOs.SeriesDetail; using API.DTOs.Settings; @@ -31,36 +26,25 @@ using API.Extensions.QueryExtensions.Filtering; using API.Helpers; using API.Helpers.Converters; using API.Services; -using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Scanner; using AutoMapper; using AutoMapper.QueryableExtensions; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using SQLite; namespace API.Data.Repositories; -#nullable enable [Flags] public enum SeriesIncludes { None = 1, Volumes = 2, - /// - /// This will include all necessary includes - /// Metadata = 4, Related = 8, Library = 16, - Chapters = 32, - ExternalReviews = 64, - ExternalRatings = 128, - ExternalRecommendations = 256, - ExternalMetadata = 512, - - ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, + Chapters = 32 } /// @@ -71,7 +55,6 @@ public enum QueryContext { None = 1, Search = 2, - [Obsolete("Use Dashboard")] Recommended = 3, Dashboard = 4, } @@ -80,12 +63,9 @@ public interface ISeriesRepository { void Add(Series series); void Attach(Series series); - void Attach(SeriesRelation relation); void Update(Series series); - void Update(SeriesMetadata seriesMetadata); void Remove(Series series); void Remove(IEnumerable series); - void Detach(Series series); Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format); /// /// Adds user information like progress, ratings, etc @@ -103,14 +83,12 @@ public interface ISeriesRepository /// /// /// - /// Includes Files in the Search /// - Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true); + Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery); Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None); Task GetSeriesDtoByIdAsync(int seriesId, int userId); Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata); - Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user); - Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true); + Task> GetSeriesByIdsAsync(IList seriesIds); Task GetChapterIdsForSeriesAsync(IList seriesIds); Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); /// @@ -149,19 +127,12 @@ public interface ISeriesRepository Task> GetWantToReadForUserAsync(int userId); Task IsSeriesInWantToRead(int userId, int seriesId); Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None); Task> GetAllSeriesByNameAsync(IList normalizedNames, int userId, SeriesIncludes includes = SeriesIncludes.None); Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); - - Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - public Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format); Task> RemoveSeriesNotInList(IList seenSeries, int libraryId); Task>> GetFolderPathMap(int libraryId); - Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); + Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); /// /// This is only used for /// @@ -169,30 +140,25 @@ public interface ISeriesRepository Task> GetLibraryIdsForSeriesAsync(); Task> GetSeriesMetadataForIds(IEnumerable seriesIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); - Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); + Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); Task GetAverageUserRating(int seriesId, int userId); Task RemoveFromOnDeck(int seriesId, int userId); Task ClearOnDeckRemoval(int seriesId, int userId); - Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None); - Task GetPlusSeriesDto(int seriesId); - Task GetCountAsync(); - Task MatchSeries(ExternalSeriesDetailDto externalSeries); + Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto); } public class SeriesRepository : ISeriesRepository { private readonly DataContext _context; private readonly IMapper _mapper; - private readonly UserManager _userManager; private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - public SeriesRepository(DataContext context, IMapper mapper, UserManager userManager) + public SeriesRepository(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; - _userManager = userManager; } public void Add(Series series) @@ -205,26 +171,11 @@ public class SeriesRepository : ISeriesRepository _context.Series.Attach(series); } - public void Attach(SeriesRelation relation) - { - _context.SeriesRelation.Attach(relation); - } - - public void Attach(ExternalSeriesMetadata metadata) - { - _context.ExternalSeriesMetadata.Attach(metadata); - } - public void Update(Series series) { _context.Entry(series).State = EntityState.Modified; } - public void Update(SeriesMetadata seriesMetadata) - { - _context.Entry(seriesMetadata).State = EntityState.Modified; - } - public void Remove(Series series) { _context.Series.Remove(series); @@ -235,11 +186,6 @@ public class SeriesRepository : ISeriesRepository _context.Series.RemoveRange(series); } - public void Detach(Series series) - { - _context.Entry(series).State = EntityState.Detached; - } - /// /// Returns if a series name and format exists already in a library /// @@ -379,24 +325,30 @@ public class SeriesRepository : ISeriesRepository return await _context.Library.GetUserLibraries(userId, queryContext).ToListAsync(); } - return [libraryId]; + return new List() + { + libraryId + }; } - public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true) + public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery) { const int maxRecords = 15; var result = new SearchResultGroupDto(); var searchQueryNormalized = searchQuery.ToNormalized(); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var seriesIds = await _context.Series + var seriesIds = _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) - .ToListAsync(); + .ToList(); result.Libraries = await _context.Library - .Search(searchQuery, userId, libraryIds) + .Where(l => libraryIds.Contains(l.Id)) + .Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%")) + .IsRestricted(QueryContext.Search) + .AsSplitQuery() .Take(maxRecords) .OrderBy(l => l.Name.ToLower()) .ProjectTo(_mapper.ConfigurationProvider) @@ -417,117 +369,91 @@ public class SeriesRepository : ISeriesRepository .Include(s => s.Library) .AsNoTracking() .AsSplitQuery() - .OrderBy(s => s.SortName!.ToLower()) .Take(maxRecords) + .OrderBy(s => s.SortName!.ToLower()) .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); - result.Bookmarks = (await _context.AppUserBookmark - .Join( - _context.Series, - bookmark => bookmark.SeriesId, - series => series.Id, - (bookmark, series) => new {Bookmark = bookmark, Series = series} - ) - .Where(joined => joined.Bookmark.AppUserId == userId && - (EF.Functions.Like(joined.Series.Name, $"%{searchQuery}%") || - (joined.Series.OriginalName != null && - EF.Functions.Like(joined.Series.OriginalName, $"%{searchQuery}%")) || - (joined.Series.LocalizedName != null && - EF.Functions.Like(joined.Series.LocalizedName, $"%{searchQuery}%")))) - .OrderBy(joined => joined.Series.Name) - .Take(maxRecords) - .Select(joined => new BookmarkSearchResultDto() - { - SeriesName = joined.Series.Name, - LocalizedSeriesName = joined.Series.LocalizedName, - LibraryId = joined.Series.LibraryId, - SeriesId = joined.Bookmark.SeriesId, - ChapterId = joined.Bookmark.ChapterId, - VolumeId = joined.Bookmark.VolumeId - }) - .ToListAsync()).DistinctBy(s => s.SeriesId); - - result.ReadingLists = await _context.ReadingList - .Search(searchQuery, userId, userRating) + .Where(rl => rl.AppUserId == userId || rl.Promoted) + .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) + .RestrictAgainstAgeRestriction(userRating) + .AsSplitQuery() .Take(maxRecords) + .OrderBy(r => r.NormalizedTitle) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - result.Collections = await _context.AppUserCollection - .Search(searchQuery, userId, userRating) + result.Collections = await _context.CollectionTag + .Where(c => (EF.Functions.Like(c.Title, $"%{searchQuery}%")) + || (EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))) + .Where(c => c.Promoted || isAdmin) + .RestrictAgainstAgeRestriction(userRating) + .OrderBy(s => s.NormalizedTitle) + .AsNoTracking() + .AsSplitQuery() .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - // I can't work out how to map people in DB layer - var personIds = await _context.SeriesMetadata - .SearchPeople(searchQuery, seriesIds) - .Select(p => p.Id) + result.Persons = await _context.SeriesMetadata + .Where(sm => seriesIds.Contains(sm.SeriesId)) + .SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%"))) + .AsSplitQuery() .Distinct() - .OrderBy(id => id) .Take(maxRecords) - .ToListAsync(); - - result.Persons = await _context.Person - .Where(p => personIds.Contains(p.Id)) .OrderBy(p => p.NormalizedName) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); result.Genres = await _context.SeriesMetadata - .SearchGenres(searchQuery, seriesIds) + .Where(sm => seriesIds.Contains(sm.SeriesId)) + .SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) + .AsSplitQuery() + .Distinct() .Take(maxRecords) + .OrderBy(t => t.NormalizedTitle) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); result.Tags = await _context.SeriesMetadata - .SearchTags(searchQuery, seriesIds) + .Where(sm => seriesIds.Contains(sm.SeriesId)) + .SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) + .AsSplitQuery() + .Distinct() .Take(maxRecords) + .OrderBy(t => t.NormalizedTitle) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - result.Files = []; - result.Chapters = (List) []; + var fileIds = _context.Series + .Where(s => seriesIds.Contains(s.Id)) + .AsSplitQuery() + .SelectMany(s => s.Volumes) + .SelectMany(v => v.Chapters) + .SelectMany(c => c.Files.Select(f => f.Id)); + + result.Files = await _context.MangaFile + .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) + .AsSplitQuery() + .Take(maxRecords) + .OrderBy(f => f.FilePath) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); - if (includeChapterAndFiles) - { - var fileIds = _context.Series - .Where(s => seriesIds.Contains(s.Id)) - .AsSplitQuery() - .SelectMany(s => s.Volumes) - .SelectMany(v => v.Chapters) - .SelectMany(c => c.Files.Select(f => f.Id)); - - // Need to check if an admin - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - if (await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole)) - { - result.Files = await _context.MangaFile - .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) - .AsSplitQuery() - .OrderBy(f => f.FilePath) - .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - result.Chapters = await _context.Chapter - .Include(c => c.Files) - .Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%") - || EF.Functions.Like(c.ISBN, $"%{searchQuery}%") - || EF.Functions.Like(c.Range, $"%{searchQuery}%") + result.Chapters = await _context.Chapter + .Include(c => c.Files) + .Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%") + || EF.Functions.Like(c.ISBN, $"%{searchQuery}%") ) - .Where(c => c.Files.All(f => fileIds.Contains(f.Id))) - .AsSplitQuery() - .OrderBy(c => c.TitleName) - .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } + .Where(c => c.Files.All(f => fileIds.Contains(f.Id))) + .AsSplitQuery() + .Take(maxRecords) + .OrderBy(c => c.TitleName) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); return result; } @@ -566,58 +492,28 @@ public class SeriesRepository : ISeriesRepository .SingleOrDefaultAsync(); } - /// - /// Returns Full Series including all external links - /// - /// - /// Include all the includes or just the Series - /// - public async Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true) + public async Task GetSeriesByIdForUserAsync(int seriesId, int userId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata) { - var query = _context.Series - .Where(s => seriesIds.Contains(s.Id)) - .AsSplitQuery(); - - if (!fullSeries) return await query.ToListAsync(); - - return await query - .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalRatings) - .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalReviews) - .Include(s => s.Relations) - .Include(s => s.Metadata) - - .Include(s => s.ExternalSeriesMetadata) - - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(e => e.ExternalRatings) - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(e => e.ExternalReviews) - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(e => e.ExternalRecommendations) - .ToListAsync(); + return await _context.Series + .Where(s => s.Id == seriesId) + .Includes(includes) + .SingleOrDefaultAsync(); } - public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user) + /// + /// Returns Volumes, Metadata, and Collection Tags + /// + /// + /// + public async Task> GetSeriesByIdsAsync(IList seriesIds) { - var allowedLibraries = await _context.Library - .Where(library => library.AppUsers.Any(x => x.Id == user.Id)) - .Select(l => l.Id) - .ToListAsync(); - var restriction = new AgeRestriction() - { - AgeRating = user.AgeRestriction, - IncludeUnknowns = user.AgeRestrictionIncludeUnknowns - }; return await _context.Series + .Include(s => s.Volumes) .Include(s => s.Metadata) - .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) - .RestrictAgainstAgeRestriction(restriction) + .ThenInclude(m => m.CollectionTags) + .Include(s => s.Relations) + .Where(s => seriesIds.Contains(s.Id)) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } @@ -693,7 +589,6 @@ public class SeriesRepository : ISeriesRepository .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) - .ThenInclude(p => p.Person) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -726,47 +621,18 @@ public class SeriesRepository : ISeriesRepository return await query.ToListAsync(); } - public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None) + public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto) { - var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext); + var query = await CreateFilteredSearchQueryableV2(userId, filterDto, QueryContext.None); var retSeries = query .ProjectTo(_mapper.ConfigurationProvider) + .AsSplitQuery() .AsNoTracking(); return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); } - public async Task GetPlusSeriesDto(int seriesId) - { - return await _context.Series - .Where(s => s.Id == seriesId) - .Include(s => s.ExternalSeriesMetadata) - .Select(series => new PlusSeriesRequestDto() - { - MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - SeriesName = series.Name, - AltSeriesName = series.LocalizedName, - AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.AniListWeblinkWebsite), - MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MalWeblinkWebsite), - CbrId = series.ExternalSeriesMetadata.CbrId, - GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MangaDexWeblinkWebsite), - VolumeCount = series.Volumes.Count, - ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), - Year = series.Metadata.ReleaseYear - }) - .FirstOrDefaultAsync(); - } - - public async Task GetCountAsync() - { - return await _context.Series.CountAsync(); - } public async Task AddSeriesModifiers(int userId, IList series) { @@ -996,20 +862,6 @@ public class SeriesRepository : ISeriesRepository out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter, out var hasPublicationFilter, out var hasSeriesNameFilter, out var hasReleaseYearMinFilter, out var hasReleaseYearMaxFilter); - IList collectionSeries = []; - if (hasCollectionTagFilter) - { - collectionSeries = await _context.AppUserCollection - .Where(uc => uc.Promoted || uc.AppUserId == userId) - .Where(uc => filter.CollectionTags.Contains(uc.Id)) - .SelectMany(uc => uc.Items) - .RestrictAgainstAgeRestriction(userRating) - .Select(s => s.Id) - .Distinct() - .ToListAsync(); - } - - var query = _context.Series .AsNoTracking() // This new style can handle any filterComparision coming from the user @@ -1017,15 +869,15 @@ public class SeriesRepository : ISeriesRepository .HasReleaseYear(hasReleaseYearMaxFilter, FilterComparison.LessThanEqual, filter.ReleaseYearRange?.Max) .HasReleaseYear(hasReleaseYearMinFilter, FilterComparison.GreaterThanEqual, filter.ReleaseYearRange?.Min) .HasName(hasSeriesNameFilter, FilterComparison.Matches, filter.SeriesNameQuery) - .HasRating(hasRatingFilter, FilterComparison.GreaterThanEqual, filter.Rating / 100f, userId) + .HasRating(hasRatingFilter, FilterComparison.GreaterThanEqual, filter.Rating, userId) .HasAgeRating(hasAgeRating, FilterComparison.Contains, filter.AgeRating) .HasPublicationStatus(hasPublicationFilter, FilterComparison.Contains, filter.PublicationStatus) .HasTags(hasTagsFilter, FilterComparison.Contains, filter.Tags) - .HasCollectionTags(hasCollectionTagFilter, FilterComparison.Contains, filter.Tags, collectionSeries) + .HasCollectionTags(hasCollectionTagFilter, FilterComparison.Contains, filter.Tags) .HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres) .HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!) .HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0) - .HasPeopleLegacy(hasPeopleFilter, FilterComparison.Contains, allPeopleIds) + .HasPeople(hasPeopleFilter, FilterComparison.Contains, allPeopleIds) .WhereIf(onlyParentSeries, s => s.RelationOf.Count == 0 || s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel)) @@ -1071,8 +923,6 @@ public class SeriesRepository : ISeriesRepository SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, filter.SortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, filter.SortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max(), filter.SortOptions), - SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings - .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), filter.SortOptions), _ => query }; @@ -1096,18 +946,17 @@ public class SeriesRepository : ISeriesRepository return query.Where(s => false); } + + // First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here query = ApplyLibraryFilter(filter, query); query = ApplyWantToReadFilter(filter, query, userId); - query = await ApplyCollectionFilter(filter, query, userId, userRating); - - - query = BuildFilterQuery(userId, filter, query); + query = query .WhereIf(userLibraries.Count > 0, s => userLibraries.Contains(s.LibraryId)) .WhereIf(onlyParentSeries, s => @@ -1118,52 +967,7 @@ public class SeriesRepository : ISeriesRepository return ApplyLimit(query .Sort(userId, filter.SortOptions) - .AsSplitQuery() - , filter.LimitTo); - } - - private async Task> ApplyCollectionFilter(FilterV2Dto filter, IQueryable query, int userId, AgeRestriction userRating) - { - var collectionStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.CollectionTags); - if (collectionStmt == null) return query; - - var value = (IList) FilterFieldValueConverter.ConvertValue(collectionStmt.Field, collectionStmt.Value); - - if (value.Count == 0) - { - return query; - } - - var collectionSeries = await _context.AppUserCollection - .Where(uc => uc.Promoted || uc.AppUserId == userId) - .Where(uc => value.Contains(uc.Id)) - .SelectMany(uc => uc.Items) - .RestrictAgainstAgeRestriction(userRating) - .Select(s => s.Id) - .Distinct() - .ToListAsync(); - - if (collectionStmt.Comparison != FilterComparison.MustContains) - return query.HasCollectionTags(true, collectionStmt.Comparison, value, collectionSeries); - - var collectionSeriesTasks = value.Select(async collectionId => - { - return await _context.AppUserCollection - .Where(uc => uc.Promoted || uc.AppUserId == userId) - .Where(uc => uc.Id == collectionId) - .SelectMany(uc => uc.Items) - .RestrictAgainstAgeRestriction(userRating) - .Select(s => s.Id) - .ToListAsync(); - }); - - var collectionSeriesLists = await Task.WhenAll(collectionSeriesTasks); - - // Find the common series among all collections - var commonSeries = collectionSeriesLists.Aggregate((common, next) => common.Intersect(next).ToList()); - - // Filter the original query based on the common series - return query.Where(s => commonSeries.Contains(s.Id)); + .AsSplitQuery(), filter.LimitTo); } private IQueryable ApplyWantToReadFilter(FilterV2Dto filter, IQueryable query, int userId) @@ -1171,10 +975,7 @@ public class SeriesRepository : ISeriesRepository var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead); if (wantToReadStmt == null) return query; - var seriesIds = _context.AppUser.Where(u => u.Id == userId) - .SelectMany(u => u.WantToRead) - .Select(s => s.SeriesId); - + var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id); if (bool.Parse(wantToReadStmt.Value)) { query = query.Where(s => seriesIds.Contains(s.Id)); @@ -1191,7 +992,6 @@ public class SeriesRepository : ISeriesRepository { var filterIncludeLibs = new List(); var filterExcludeLibs = new List(); - if (filter.Statements != null) { foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries)) @@ -1233,7 +1033,7 @@ public class SeriesRepository : ISeriesRepository private static IQueryable BuildFilterQuery(int userId, FilterV2Dto filterDto, IQueryable query) { - if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query; + if (filterDto.Statements == null || !filterDto.Statements.Any()) return query; var queries = filterDto.Statements @@ -1252,7 +1052,6 @@ public class SeriesRepository : ISeriesRepository private static IQueryable BuildFilterGroup(int userId, FilterStatementDto statement, IQueryable query) { - var value = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); return statement.Field switch { @@ -1264,25 +1063,20 @@ public class SeriesRepository : ISeriesRepository (IList) value), FilterField.Languages => query.HasLanguage(true, statement.Comparison, (IList) value), FilterField.AgeRating => query.HasAgeRating(true, statement.Comparison, (IList) value), - FilterField.UserRating => query.HasRating(true, statement.Comparison, (float) value , userId), + FilterField.UserRating => query.HasRating(true, statement.Comparison, (int) value, userId), FilterField.Tags => query.HasTags(true, statement.Comparison, (IList) value), - FilterField.Translators => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Translator), - FilterField.Characters => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Character), - FilterField.Publisher => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Publisher), - FilterField.Editor => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Editor), - FilterField.CoverArtist => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.CoverArtist), - FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Letterer), - FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Inker), - FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Inker), - FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Imprint), - FilterField.Team => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Team), - FilterField.Location => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Location), - FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Penciller), - FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList) value, PersonRole.Writer), + FilterField.CollectionTags => query.HasCollectionTags(true, statement.Comparison, (IList) value), + FilterField.Translators => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Characters => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Publisher => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Editor => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.CoverArtist => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList) value), + FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList) value), FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList) value), - FilterField.CollectionTags => - // This is handled in the code before this as it's handled in a more general, combined manner - query, FilterField.Libraries => // This is handled in the code before this as it's handled in a more general, combined manner query, @@ -1294,9 +1088,7 @@ public class SeriesRepository : ISeriesRepository FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value), FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value), FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId), - FilterField.ReadLast => query.HasReadLast(true, statement.Comparison, (int) value, userId), - FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value), - _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") + _ => throw new ArgumentOutOfRangeException() }; } @@ -1311,7 +1103,7 @@ public class SeriesRepository : ISeriesRepository var query = sQuery .WhereIf(hasGenresFilter, s => s.Metadata.Genres.Any(g => filter.Genres.Contains(g.Id))) - .WhereIf(hasPeopleFilter, s => s.Metadata.People.Any(p => allPeopleIds.Contains(p.PersonId))) + .WhereIf(hasPeopleFilter, s => s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id))) .WhereIf(hasCollectionTagFilter, s => s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id))) .WhereIf(hasRatingFilter, s => s.Ratings.Any(r => r.Rating >= filter.Rating && r.AppUserId == userId)) @@ -1335,30 +1127,51 @@ public class SeriesRepository : ISeriesRepository public async Task GetSeriesMetadata(int seriesId) { - return await _context.SeriesMetadata + var metadataDto = await _context.SeriesMetadata .Where(metadata => metadata.SeriesId == seriesId) .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) - .ThenInclude(p => p.Person) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() .SingleOrDefaultAsync(); + + if (metadataDto != null) + { + metadataDto.CollectionTags = await _context.CollectionTag + .Include(t => t.SeriesMetadatas) + .Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId)) + .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() + .OrderBy(t => t.Title.ToLower()) + .AsSplitQuery() + .ToListAsync(); + } + + return metadataDto; } public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = _context.Library + .Include(l => l.AppUsers) + .Where(library => library.AppUsers.Any(user => user.Id == userId)) + .AsSplitQuery() + .AsNoTracking() + .Select(library => library.Id) + .ToList(); - var query = _context.AppUserCollection + var query = _context.CollectionTag .Where(s => s.Id == collectionId) - .Include(c => c.Items) - .SelectMany(c => c.Items.Where(s => userLibraries.Contains(s.LibraryId))) + .Include(c => c.SeriesMetadatas) + .ThenInclude(m => m.Series) + .SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId))) .OrderBy(s => s.LibraryId) .ThenBy(s => s.SortName.ToLower()) .ProjectTo(_mapper.ConfigurationProvider) - .AsSplitQuery(); + .AsSplitQuery() + .AsNoTracking(); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } @@ -1382,10 +1195,8 @@ public class SeriesRepository : ISeriesRepository .Where(library => library.AppUsers.Any(x => x.Id == userId)) .AsSplitQuery() .Select(l => l.Id); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Series - .RestrictAgainstAgeRestriction(userRating) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .OrderBy(s => s.SortName.ToLower()) .ProjectTo(_mapper.ConfigurationProvider) @@ -1550,7 +1361,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams) { - var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); @@ -1631,44 +1442,14 @@ public class SeriesRepository : ISeriesRepository /// /// Return a Series by Folder path. Null if not found. /// - /// This will be normalized in the query and checked against FolderPath and LowestFolderPath + /// This will be normalized in the query /// Additional relationships to include with the base query /// public async Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None) { var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder); - if (string.IsNullOrEmpty(normalized)) return null; - return await _context.Series - .Where(s => (!string.IsNullOrEmpty(s.FolderPath) && s.FolderPath.Equals(normalized) || (!string.IsNullOrEmpty(s.LowestFolderPath) && s.LowestFolderPath.Equals(normalized)))) - .Includes(includes) - .SingleOrDefaultAsync(); - } - - public async Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None) - { - // Check if the path ends with a file (has a file extension) - string directoryPath; - if (Path.HasExtension(path)) - { - // Remove the file part and get the directory path - directoryPath = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(directoryPath)) return null; - } - else - { - // Use the path as is if it doesn't end with a file - directoryPath = path; - } - - // Normalize the directory path - var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(directoryPath); - if (string.IsNullOrEmpty(normalized)) return null; - - normalized = normalized.TrimEnd('/'); - - return await _context.Series - .Where(s => !string.IsNullOrEmpty(s.LowestFolderPath) && EF.Functions.Like(normalized, s.LowestFolderPath + "%")) + .Where(s => s.FolderPath != null && s.FolderPath.Equals(normalized)) .Includes(includes) .SingleOrDefaultAsync(); } @@ -1726,7 +1507,6 @@ public class SeriesRepository : ISeriesRepository .Include(s => s.Metadata) .ThenInclude(m => m.People) - .ThenInclude(p => p.Person) .Include(s => s.Metadata) .ThenInclude(m => m.Genres) @@ -1737,7 +1517,6 @@ public class SeriesRepository : ISeriesRepository .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) .ThenInclude(cm => cm.People) - .ThenInclude(p => p.Person) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) @@ -1753,99 +1532,9 @@ public class SeriesRepository : ISeriesRepository .AsSplitQuery(); return query.SingleOrDefaultAsync(); - #nullable enable } - public async Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) - { - var libraryIds = GetLibraryIdsForUser(userId); - var normalizedSeries = seriesName.ToNormalized(); - var normalizedLocalized = localizedName.ToNormalized(); - - var query = _context.Series - .Where(s => libraryIds.Contains(s.LibraryId)) - .Where(s => formats.Contains(s.Format)); - - if (aniListId.HasValue && aniListId.Value > 0) - { - // If AniList ID is provided, override name checks - query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value); - } - else - { - // Otherwise, use name checks - query = query.Where(s => - s.NormalizedName.Equals(normalizedSeries) - || s.NormalizedName.Equals(normalizedLocalized) - || s.NormalizedLocalizedName.Equals(normalizedSeries) - || (!string.IsNullOrEmpty(normalizedLocalized) && s.NormalizedLocalizedName.Equals(normalizedLocalized)) - || (s.OriginalName != null && s.OriginalName.Equals(seriesName)) - ); - } - - return await query - .Includes(includes) - .FirstOrDefaultAsync(); - } - - - public async Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) - { - var libraryIds = GetLibraryIdsForUser(userId); - names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); - var normalizedNames = names.Select(s => s.ToNormalized()).ToList(); - - - var query = _context.Series - .Where(s => libraryIds.Contains(s.LibraryId)) - .Where(s => formats.Contains(s.Format)); - - if (aniListId.HasValue && aniListId.Value > 0) - { - // If AniList ID is provided, override name checks - query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value || - normalizedNames.Contains(s.NormalizedName) - || normalizedNames.Contains(s.NormalizedLocalizedName) - || names.Contains(s.OriginalName)); - } - else - { - // Otherwise, use name checks - query = query.Where(s => - normalizedNames.Contains(s.NormalizedName) - || normalizedNames.Contains(s.NormalizedLocalizedName) - || names.Contains(s.OriginalName)); - } - - return await query - .Includes(includes) - .FirstOrDefaultAsync(); - } - - public async Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format) - { - var normalizedSeries = seriesName.ToNormalized(); - var normalizedLocalized = localizedName.ToNormalized(); - return await _context.Series - .Where(s => s.LibraryId == libraryId) - .Where(s => s.Format == format && format != MangaFormat.Unknown) - .Where(s => - s.NormalizedName.Equals(normalizedSeries) - || s.NormalizedName.Equals(normalizedLocalized) - - || s.NormalizedLocalizedName.Equals(normalizedSeries) - || (!string.IsNullOrEmpty(normalizedLocalized) && s.NormalizedLocalizedName.Equals(normalizedLocalized)) - - || (s.OriginalName != null && s.OriginalName.Equals(seriesName)) - ) - .AsSplitQuery() - .ToListAsync(); - } - /// /// Removes series that are not in the seenSeries list. Does not commit. @@ -1854,36 +1543,45 @@ public class SeriesRepository : ISeriesRepository /// public async Task> RemoveSeriesNotInList(IList seenSeries, int libraryId) { - if (!seenSeries.Any()) return Array.Empty(); - - // Get all series from DB in one go, based on libraryId - var dbSeries = await _context.Series - .Where(s => s.LibraryId == libraryId) - .ToListAsync(); - - // Get a set of matching series ids for the given parsedSeries - var ids = new HashSet(); + if (seenSeries.Count == 0) return Array.Empty(); + var ids = new List(); foreach (var parsedSeries in seenSeries) { - var matchingSeries = dbSeries - .Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName) - .OrderBy(s => s.Id) // Sort to handle potential duplicates - .ToList(); - - // Prefer the first match or handle duplicates by choosing the last one - if (matchingSeries.Count != 0) + try { - ids.Add(matchingSeries.Last().Id); + var seriesId = await _context.Series + .Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName && + s.LibraryId == libraryId) + .Select(s => s.Id) + .SingleOrDefaultAsync(); + if (seriesId > 0) + { + ids.Add(seriesId); + } + } + catch (Exception) + { + // This is due to v0.5.6 introducing bugs where we could have multiple series get duplicated and no way to delete them + // This here will delete the 2nd one as the first is the one to likely be used. + var sId = _context.Series + .Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName && + s.LibraryId == libraryId) + .Select(s => s.Id) + .OrderBy(s => s) + .Last(); + if (sId > 0) + { + ids.Add(sId); + } } } - // Filter out series that are not in the seenSeries - var seriesToRemove = dbSeries + var seriesToRemove = await _context.Series + .Where(s => s.LibraryId == libraryId) .Where(s => !ids.Contains(s.Id)) - .ToList(); + .ToListAsync(); - // Remove series in bulk _context.Series.RemoveRange(seriesToRemove); return seriesToRemove; @@ -1986,7 +1684,19 @@ public class SeriesRepository : ISeriesRepository AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating), AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating), Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating), - Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating), + // Parent = await _context.Series + // .SelectMany(s => + // s.TargetSeries.Where(r => r.TargetSeriesId == seriesId + // && usersSeriesIds.Contains(r.TargetSeriesId) + // && r.RelationKind != RelationKind.Prequel + // && r.RelationKind != RelationKind.Sequel + // && r.RelationKind != RelationKind.Edition) + // .Select(sr => sr.Series)) + // .RestrictAgainstAgeRestriction(userRating) + // .AsSplitQuery() + // .AsNoTracking() + // .ProjectTo(_mapper.ConfigurationProvider) + // .ToListAsync(), Parent = await _context.SeriesRelation .Where(r => r.TargetSeriesId == seriesId && usersSeriesIds.Contains(r.TargetSeriesId) @@ -2048,10 +1758,10 @@ public class SeriesRepository : ISeriesRepository VolumeId = c.VolumeId, ChapterId = c.Id, Format = c.Volume.Series.Format, - ChapterNumber = c.MinNumber + string.Empty, // TODO: Refactor this - ChapterRange = c.Range, // TODO: Refactor this + ChapterNumber = c.Number, + ChapterRange = c.Range, IsSpecial = c.IsSpecial, - VolumeNumber = c.Volume.MinNumber, + VolumeNumber = c.Volume.Number, ChapterTitle = c.Title, AgeRating = c.Volume.Series.Metadata.AgeRating }) @@ -2067,8 +1777,7 @@ public class SeriesRepository : ISeriesRepository var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.Series.LibraryId)) - .Select(w => w.Series) + .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .AsNoTracking(); @@ -2080,25 +1789,16 @@ public class SeriesRepository : ISeriesRepository public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter) { var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var seriesIds = await _context.AppUser + var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.Series.LibraryId)) - .Select(w => w.Series.Id) - .Distinct() - .ToListAsync(); - - var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None); - - // Apply the Want to Read filtering - query = query.Where(s => seriesIds.Contains(s.Id)); - - var retSeries = query - .ProjectTo(_mapper.ConfigurationProvider) + .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + var filteredQuery = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None, query); + + return await PagedList.CreateAsync(filteredQuery.ProjectTo(_mapper.ConfigurationProvider), userParams.PageNumber, userParams.PageSize); } public async Task> GetWantToReadForUserAsync(int userId) @@ -2107,30 +1807,28 @@ public class SeriesRepository : ISeriesRepository return await _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.Series.LibraryId)) - .Select(w => w.Series) + .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .AsNoTracking() .ToListAsync(); } /// - /// Uses multiple names to find a match against a series. If not, returns null. + /// Uses multiple names to find a match against a series then ensures the user has appropriate access to it. If not, returns null. /// - /// This does not restrict to the user at all. That is handled at the API level. - public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) + /// + /// + /// + public async Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) { - var libraryIds = await _context.Library - .Where(lib => lib.Type == libraryType) - .Select(l => l.Id) - .ToListAsync(); - + var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = await _context.Library.GetUserLibrariesByType(userId, libraryType).ToListAsync(); var normalizedNames = names.Select(n => n.ToNormalized()).ToList(); SeriesDto? result = null; if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl)) { - // TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here result = await _context.Series + .RestrictAgainstAgeRestriction(userRating) .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl)) @@ -2143,6 +1841,7 @@ public class SeriesRepository : ISeriesRepository if (result != null) return result; return await _context.Series + .RestrictAgainstAgeRestriction(userRating) .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) @@ -2151,47 +1850,6 @@ public class SeriesRepository : ISeriesRepository .FirstOrDefaultAsync(); // Some users may have improperly configured libraries } - public async Task MatchSeries(ExternalSeriesDetailDto externalSeries) - { - var libraryIds = await _context.Library - .Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type)) - .Select(l => l.Id) - .ToListAsync(); - - var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty()) - .Prepend(externalSeries.Name) - .Select(n => n.ToNormalized()) - .ToList(); - - var aniListWebLink = - ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId); - var malWebLink = - ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId); - - Series? result = null; - if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink)) - { - result = await _context.Series - .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) - .Where(s => libraryIds.Contains(s.Library.Id)) - .WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink)) - .WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink)) - .Include(s => s.Metadata) - .AsSplitQuery() - .FirstOrDefaultAsync(); - } - - if (result != null) return result; - - return await _context.Series - .Where(s => normalizedNames.Contains(s.NormalizedName) || - normalizedNames.Contains(s.NormalizedLocalizedName)) - .Where(s => libraryIds.Contains(s.Library.Id)) - .AsSplitQuery() - .Include(s => s.Metadata) - .FirstOrDefaultAsync(); // Some users may have improperly configured libraries - } - /// /// Returns the Average rating for all users within Kavita instance /// @@ -2200,8 +1858,7 @@ public class SeriesRepository : ISeriesRepository { // If there is 0 or 1 rating and that rating is you, return 0 back var countOfRatingsThatAreUser = await _context.AppUserRating - .Where(r => r.SeriesId == seriesId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); + .Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId); if (countOfRatingsThatAreUser == 1) { return 0; @@ -2241,7 +1898,7 @@ public class SeriesRepository : ISeriesRepository var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); return await _context.AppUser .Where(user => user.Id == userId) - .SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId))) + .SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId))) .AsSplitQuery() .AsNoTracking() .AnyAsync(); @@ -2258,17 +1915,15 @@ public class SeriesRepository : ISeriesRepository LastScanned = s.LastFolderScanned, SeriesName = s.Name, FolderPath = s.FolderPath, - LowestFolderPath = s.LowestFolderPath, Format = s.Format, LibraryRoots = s.Library.Folders.Select(f => f.Path) - }) - .ToListAsync(); + }).ToListAsync(); var map = new Dictionary>(); foreach (var series in info) { - if (string.IsNullOrEmpty(series.FolderPath)) continue; - if (!map.TryGetValue(series.FolderPath, out var value)) + if (series.FolderPath == null) continue; + if (!map.ContainsKey(series.FolderPath)) { map.Add(series.FolderPath, new List() { @@ -2277,42 +1932,27 @@ public class SeriesRepository : ISeriesRepository } else { - value.Add(series); + map[series.FolderPath].Add(series); } - - if (string.IsNullOrEmpty(series.LowestFolderPath) || series.FolderPath.Equals(series.LowestFolderPath)) continue; - if (!map.TryGetValue(series.LowestFolderPath, out var value2)) - { - map.Add(series.LowestFolderPath, new List() - { - series - }); - } - else - { - value2.Add(series); - } } return map; } /// - /// Returns the highest Age Rating for a list of Series. Defaults to + /// Returns the highest Age Rating for a list of Series /// /// /// - public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) + public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) { - var ret = await _context.Series + return await _context.Series .Where(s => seriesIds.Contains(s.Id)) .Include(s => s.Metadata) .Select(s => s.Metadata.AgeRating) .OrderBy(s => s) .LastOrDefaultAsync(); - if (ret == null) return AgeRating.Unknown; - return ret; } /// diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index 90246e75f..f142947b1 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -1,32 +1,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.SeriesDetail; using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; using AutoMapper; -using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface ISettingsRepository { void Update(ServerSetting settings); - void Update(MetadataSettings settings); - void RemoveRange(List fieldMappings); Task GetSettingsDtoAsync(); Task GetSettingAsync(ServerSettingKey key); Task> GetSettingsAsync(); void Remove(ServerSetting setting); - Task GetExternalSeriesMetadata(int seriesId); - Task GetMetadataSettings(); - Task GetMetadataSettingDto(); } public class SettingsRepository : ISettingsRepository { @@ -44,43 +33,11 @@ public class SettingsRepository : ISettingsRepository _context.Entry(settings).State = EntityState.Modified; } - public void Update(MetadataSettings settings) - { - _context.Entry(settings).State = EntityState.Modified; - } - - public void RemoveRange(List fieldMappings) - { - _context.MetadataFieldMapping.RemoveRange(fieldMappings); - } - public void Remove(ServerSetting setting) { _context.Remove(setting); } - public async Task GetExternalSeriesMetadata(int seriesId) - { - return await _context.ExternalSeriesMetadata - .Where(s => s.SeriesId == seriesId) - .FirstOrDefaultAsync(); - } - - public async Task GetMetadataSettings() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .FirstAsync(); - } - - public async Task GetMetadataSettingDto() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstAsync(); - } - public async Task GetSettingsDtoAsync() { var settings = await _context.ServerSetting diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index 33517e846..4e1a01c98 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -8,7 +8,6 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface ISiteThemeRepository { @@ -20,8 +19,6 @@ public interface ISiteThemeRepository Task GetThemeDtoByName(string themeName); Task GetDefaultTheme(); Task> GetThemes(); - Task GetTheme(int themeId); - Task IsThemeInUse(int themeId); } public class SiteThemeRepository : ISiteThemeRepository @@ -91,19 +88,6 @@ public class SiteThemeRepository : ISiteThemeRepository .ToListAsync(); } - public async Task GetTheme(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .FirstOrDefaultAsync(); - } - - public async Task IsThemeInUse(int themeId) - { - return await _context.AppUserPreferences - .AnyAsync(p => p.Theme.Id == themeId); - } - public async Task GetThemeDto(int themeId) { return await _context.SiteTheme diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 40d40a675..7544694ea 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -2,30 +2,23 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface ITagRepository { void Attach(Tag tag); void Remove(Tag tag); Task> GetAllTagsAsync(); - Task> GetAllTagsByNameAsync(IEnumerable normalizedNames); Task> GetAllTagDtosAsync(int userId); Task RemoveAllTagNoLongerAssociated(); - Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); - Task> GetAllTagsNotInListAsync(ICollection tags); - Task> GetBrowseableTag(int userId, UserParams userParams); + Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId); } public class TagRepository : ITagRepository @@ -63,18 +56,11 @@ public class TagRepository : ITagRepository await _context.SaveChangesAsync(); } - public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null) + public async Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - if (libraryIds is {Count: > 0}) - { - userLibs = userLibs.Where(libraryIds.Contains).ToList(); - } - return await _context.Series - .Where(s => userLibs.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Tags) .AsSplitQuery() @@ -85,74 +71,11 @@ public class TagRepository : ITagRepository .ToListAsync(); } - public async Task> GetAllTagsNotInListAsync(ICollection tags) - { - // Create a dictionary mapping normalized names to non-normalized names - var normalizedToOriginalMap = tags.Distinct() - .GroupBy(Parser.Normalize) - .ToDictionary(group => group.Key, group => group.First()); - - var normalizedTagNames = normalizedToOriginalMap.Keys.ToList(); - - // Query the database for existing genres using the normalized names - var existingTags = await _context.Tag - .Where(g => normalizedTagNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field - .Select(g => g.NormalizedTitle) - .ToListAsync(); - - // Find the normalized genres that do not exist in the database - var missingTags = normalizedTagNames.Except(existingTags).ToList(); - - // Return the original non-normalized genres for the missing ones - return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); - } - - public async Task> GetBrowseableTag(int userId, UserParams userParams) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); - - var query = _context.Tag - .RestrictAgainstAgeRestriction(ageRating) - .WhereIf(userLibs.Count != allLibrariesCount, - tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || - tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) - .Select(g => new BrowseTagDto - { - Id = g.Id, - Title = g.Title, - SeriesCount = g.SeriesMetadatas - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = g.Chapters - .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count() - }) - .OrderBy(g => g.Title); - - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); - } - public async Task> GetAllTagsAsync() { return await _context.Tag.ToListAsync(); } - public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames) - { - return await _context.Tag - .Where(t => normalizedNames.Contains(t.NormalizedTitle)) - .ToListAsync(); - } - public async Task> GetAllTagDtosAsync(int userId) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 6437cfcfe..d172ef8ba 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -7,7 +7,6 @@ using API.DTOs; using API.DTOs.Account; using API.DTOs.Dashboard; using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Account; using API.DTOs.Reader; using API.DTOs.Scrobbling; using API.DTOs.SeriesDetail; @@ -16,14 +15,12 @@ using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable [Flags] public enum AppUserIncludes @@ -41,9 +38,7 @@ public enum AppUserIncludes SmartFilters = 1024, DashboardStreams = 2048, SideNavStreams = 4096, - ExternalSources = 8192, - Collections = 16384, // 2^14 - ChapterRatings = 1 << 15, + ExternalSources = 8192 // 2^13 } public interface IUserRepository @@ -58,17 +53,12 @@ public interface IUserRepository void Delete(AppUser? user); void Delete(AppUserBookmark bookmark); void Delete(IEnumerable streams); - void Delete(AppUserDashboardStream stream); void Delete(IEnumerable streams); - void Delete(AppUserSideNavStream stream); Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true); Task> GetAdminUsersAsync(); Task IsUserAdminAsync(AppUser? user); - Task> GetRoles(int userId); Task GetUserRatingAsync(int seriesId, int userId); - Task GetUserChapterRatingAsync(int userId, int chapterId); Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); - Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId); Task GetPreferencesAsync(string username); Task> GetBookmarkDtosForSeries(int userId, int seriesId); Task> GetBookmarkDtosForVolume(int userId, int volumeId); @@ -82,13 +72,13 @@ public interface IUserRepository Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserIdByUsernameAsync(string username); Task> GetAllBookmarksByIds(IList bookmarkIds); - Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None); + Task GetUserByEmailAsync(string email); Task> GetAllPreferencesByThemeAsync(int themeId); Task HasAccessToLibrary(int libraryId, int userId); Task HasAccessToSeries(int userId, int seriesId); - Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true); + Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserByConfirmationToken(string token); - Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None); + Task GetDefaultAdminUser(); Task> GetSeriesWithRatings(int userId); Task> GetSeriesWithReviews(int userId); Task HasHoldOnSeries(int userId, int seriesId); @@ -100,13 +90,10 @@ public interface IUserRepository Task> GetDashboardStreamWithFilter(int filterId); Task> GetSideNavStreams(int userId, bool visibleOnly = false); Task GetSideNavStream(int streamId); - Task GetSideNavStreamWithUser(int streamId); Task> GetSideNavStreamWithFilter(int filterId); Task> GetSideNavStreamsByLibraryId(int libraryId); Task> GetSideNavStreamWithExternalSource(int externalSourceId); Task> GetDashboardStreamsByIds(IList streamIds); - Task> GetUserTokenInfo(); - Task GetUserByDeviceEmail(string deviceEmail); } public class UserRepository : IUserRepository @@ -173,21 +160,11 @@ public class UserRepository : IUserRepository _context.AppUserDashboardStream.RemoveRange(streams); } - public void Delete(AppUserDashboardStream stream) - { - _context.AppUserDashboardStream.Remove(stream); - } - public void Delete(IEnumerable streams) { _context.AppUserSideNavStream.RemoveRange(streams); } - public void Delete(AppUserSideNavStream stream) - { - _context.AppUserSideNavStream.Remove(stream); - } - /// /// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags. /// @@ -263,12 +240,10 @@ public class UserRepository : IUserRepository .ToListAsync(); } - public async Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None) + public async Task GetUserByEmailAsync(string email) { var lowerEmail = email.ToLower(); - return await _context.AppUser - .Includes(includes) - .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); + return await _context.AppUser.SingleOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); } @@ -304,17 +279,10 @@ public class UserRepository : IUserRepository .AnyAsync(s => s.Id == seriesId); } - public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true) + public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None) { - var query = _context.AppUser - .Includes(includeFlags); - if (track) - { - return await query.ToListAsync(); - } - - return await query - .AsNoTracking() + return await _context.AppUser + .Includes(includeFlags) .ToListAsync(); } @@ -328,13 +296,11 @@ public class UserRepository : IUserRepository /// Returns the first admin account created /// /// - public async Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None) + public async Task GetDefaultAdminUser() { - return await _context.AppUser - .Includes(includes) - .Where(u => u.UserRoles.Any(r => r.Role.Name == PolicyConstants.AdminRole)) + return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)) .OrderBy(u => u.Created) - .FirstAsync(); + .First(); } public async Task> GetSeriesWithRatings(int userId) @@ -412,13 +378,11 @@ public class UserRepository : IUserRepository .FirstOrDefaultAsync(d => d.Id == streamId); } - public async Task> GetDashboardStreamWithFilter(int filterId) { return await _context.AppUserDashboardStream .Include(d => d.SmartFilter) .Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId) - .AsSplitQuery() .ToListAsync(); } @@ -442,17 +406,16 @@ public class UserRepository : IUserRepository Order = d.Order, Visible = d.Visible }) - .AsSplitQuery() .ToListAsync(); var libraryIds = sideNavStreams.Where(d => d.StreamType == SideNavStreamType.Library) .Select(d => d.LibraryId) .ToList(); - var libraryDtos = await _context.Library + var libraryDtos = _context.Library .Where(l => libraryIds.Contains(l.Id)) .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ToList(); foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library)) { @@ -476,21 +439,13 @@ public class UserRepository : IUserRepository return sideNavStreams; } - public async Task GetSideNavStream(int streamId) + public async Task GetSideNavStream(int streamId) { return await _context.AppUserSideNavStream .Include(d => d.SmartFilter) .FirstOrDefaultAsync(d => d.Id == streamId); } - public async Task GetSideNavStreamWithUser(int streamId) - { - return await _context.AppUserSideNavStream - .Include(d => d.SmartFilter) - .Include(d => d.AppUser) - .FirstOrDefaultAsync(d => d.Id == streamId); - } - public async Task> GetSideNavStreamWithFilter(int filterId) { return await _context.AppUserSideNavStream @@ -520,47 +475,10 @@ public class UserRepository : IUserRepository .ToListAsync(); } - public async Task> GetUserTokenInfo() - { - var users = await _context.AppUser - .Select(u => new - { - u.Id, - u.UserName, - u.AniListAccessToken, // JWT Token - u.MalAccessToken // JWT Token - }) - .ToListAsync(); - - var userTokenInfos = users.Select(user => new UserTokenInfo - { - UserId = user.Id, - Username = user.UserName, - IsAniListTokenSet = !string.IsNullOrEmpty(user.AniListAccessToken), - AniListValidUntilUtc = JwtHelper.GetTokenExpiry(user.AniListAccessToken), - IsAniListTokenValid = JwtHelper.IsTokenValid(user.AniListAccessToken), - IsMalTokenSet = !string.IsNullOrEmpty(user.MalAccessToken), - }); - - return userTokenInfos; - } - - /// - /// Returns the first user with a device email matching - /// - /// - /// - public async Task GetUserByDeviceEmail(string deviceEmail) - { - return await _context.AppUser - .Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail)) - .FirstOrDefaultAsync(); - } - public async Task> GetAdminUsersAsync() { - return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc); + return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); } public async Task IsUserAdminAsync(AppUser? user) @@ -569,35 +487,11 @@ public class UserRepository : IUserRepository return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); } - public async Task> GetRoles(int userId) - { - var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); - if (user == null) return ArraySegment.Empty; - - if (_userManager == null) - { - // userManager is null on Unit Tests only - return await _context.UserRoles - .Where(ur => ur.UserId == userId) - .Select(ur => ur.Role.Name) - .ToListAsync(); - } - - return await _userManager.GetRolesAsync(user); - } - public async Task GetUserRatingAsync(int seriesId, int userId) { return await _context.AppUserRating .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) - .FirstOrDefaultAsync(); - } - - public async Task GetUserChapterRatingAsync(int userId, int chapterId) - { - return await _context.AppUserChapterRating - .Where(r => r.AppUserId == userId && r.ChapterId == chapterId) - .FirstOrDefaultAsync(); + .SingleOrDefaultAsync(); } public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) @@ -613,19 +507,6 @@ public class UserRepository : IUserRepository .ToListAsync(); } - public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) - { - return await _context.AppUserChapterRating - .Include(r => r.AppUser) - .Where(r => r.ChapterId == chapterId) - .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) - .OrderBy(r => r.AppUserId == userId) - .ThenBy(r => r.Rating) - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - public async Task GetPreferencesAsync(string username) { return await _context.AppUserPreferences @@ -746,6 +627,7 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) + .Select(o => o.Bookmark) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } @@ -757,7 +639,7 @@ public class UserRepository : IUserRepository /// - /// Fetches the AppUserId by API Key. This does not include any extra information + /// Fetches the UserId by API Key. This does not include any extra information /// /// /// @@ -784,9 +666,7 @@ public class UserRepository : IUserRepository Username = u.UserName, Email = u.Email, Created = u.Created, - CreatedUtc = u.CreatedUtc, LastActive = u.LastActive, - LastActiveUtc = u.LastActiveUtc, Roles = u.UserRoles.Select(r => r.Role.Name).ToList(), IsPending = !u.EmailConfirmed, AgeRestriction = new AgeRestrictionDto() diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index 4b07ade96..ccd909117 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -7,7 +6,6 @@ using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Extensions; -using API.Extensions.QueryExtensions; using API.Services; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -15,39 +13,22 @@ using Kavita.Common; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable - -[Flags] -public enum VolumeIncludes -{ - None = 1, - Chapters = 2, - People = 4, - Tags = 8, - /// - /// This will include Chapters by default - /// - Files = 16 -} public interface IVolumeRepository { void Add(Volume volume); void Update(Volume volume); void Remove(Volume volume); - void Remove(IList volumes); Task> GetFilesForVolume(int volumeId); Task GetVolumeCoverImageAsync(int volumeId); Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); - Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters); - Task GetVolumeAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files); + Task> GetVolumesDtoAsync(int seriesId, int userId); + Task GetVolumeAsync(int volumeId); Task GetVolumeDtoAsync(int volumeId, int userId); Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); Task> GetVolumes(int seriesId); - Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None); Task GetVolumeByIdAsync(int volumeId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task> GetCoverImagesForLockedVolumesAsync(); } public class VolumeRepository : IVolumeRepository { @@ -74,10 +55,6 @@ public class VolumeRepository : IVolumeRepository { _context.Volume.Remove(volume); } - public void Remove(IList volumes) - { - _context.Volume.RemoveRange(volumes); - } /// /// Returns a list of non-tracked files for a given volume. @@ -134,18 +111,9 @@ public class VolumeRepository : IVolumeRepository if (includeChapters) { - query = query - .Includes(VolumeIncludes.Chapters) - .AsSplitQuery(); + query = query.Include(v => v.Chapters).AsSplitQuery(); } - var volumes = await query.ToListAsync(); - - foreach (var volume in volumes) - { - volume.Chapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList(); - } - - return volumes; + return await query.ToListAsync(); } /// @@ -158,11 +126,11 @@ public class VolumeRepository : IVolumeRepository { var volume = await _context.Volume .Where(vol => vol.Id == volumeId) - .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) + .Include(vol => vol.Chapters) + .ThenInclude(c => c.Files) .AsSplitQuery() - .OrderBy(v => v.MinNumber) .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(vol => vol.Id == volumeId); + .SingleOrDefaultAsync(vol => vol.Id == volumeId); if (volume == null) return null; @@ -181,18 +149,10 @@ public class VolumeRepository : IVolumeRepository { return await _context.Volume .Where(vol => vol.SeriesId == seriesId) - .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) + .Include(vol => vol.Chapters) + .ThenInclude(c => c.Files) .AsSplitQuery() - .OrderBy(vol => vol.MinNumber) - .ToListAsync(); - } - public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None) - { - return await _context.Volume - .Where(vol => volumeIds.Contains(vol.Id)) - .Includes(includes) - .AsSplitQuery() - .OrderBy(vol => vol.MinNumber) + .OrderBy(vol => vol.Number) .ToListAsync(); } @@ -201,10 +161,11 @@ public class VolumeRepository : IVolumeRepository /// /// /// - public async Task GetVolumeAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files) + public async Task GetVolumeAsync(int volumeId) { return await _context.Volume - .Includes(includes) + .Include(vol => vol.Chapters) + .ThenInclude(c => c.Files) .AsSplitQuery() .SingleOrDefaultAsync(vol => vol.Id == volumeId); } @@ -216,37 +177,51 @@ public class VolumeRepository : IVolumeRepository /// /// /// - public async Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters) + public async Task> GetVolumesDtoAsync(int seriesId, int userId) { var volumes = await _context.Volume .Where(vol => vol.SeriesId == seriesId) - .Includes(includes) - .OrderBy(volume => volume.MinNumber) + .Include(vol => vol.Chapters) + .ThenInclude(c => c.People) + .Include(vol => vol.Chapters) + .ThenInclude(c => c.Tags) + .OrderBy(volume => volume.Number) .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() .AsSplitQuery() .ToListAsync(); await AddVolumeModifiers(userId, volumes); + SortSpecialChapters(volumes); return volumes; } public async Task GetVolumeByIdAsync(int volumeId) { - return await _context.Volume.FirstOrDefaultAsync(x => x.Id == volumeId); + return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId); } public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); return await _context.Volume - .Includes(VolumeIncludes.Chapters) + .Include(v => v.Chapters) .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .AsSplitQuery() .ToListAsync(); } + private static void SortSpecialChapters(IEnumerable volumes) + { + foreach (var v in volumes.Where(vDto => vDto.Number == 0)) + { + v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); + } + } + + private async Task AddVolumeModifiers(int userId, IReadOnlyCollection volumes) { var volIds = volumes.Select(s => s.Id); @@ -266,22 +241,7 @@ public class VolumeRepository : IVolumeRepository c.LastReadingProgress = progresses.Max(p => p.LastModified); } - v.PagesRead = userProgress - .Where(p => p.VolumeId == v.Id) - .Sum(p => p.PagesRead); + v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); } } - - /// - /// Returns cover images for locked chapters - /// - /// - public async Task> GetCoverImagesForLockedVolumesAsync() - { - return (await _context.Volume - .Where(c => c.CoverImageLocked) - .Select(c => c.CoverImage) - .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; - } } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index c08f80afa..488d58ad1 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -11,7 +10,6 @@ using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.Theme; -using API.Entities.MetadataMatching; using API.Extensions; using API.Services; using Kavita.Common; @@ -28,8 +26,8 @@ public static class Seed /// public static ImmutableArray DefaultSettings; - public static readonly ImmutableArray DefaultThemes = [ - ..new List + public static readonly ImmutableArray DefaultThemes = ImmutableArray.Create( + new List { new() { @@ -38,10 +36,8 @@ public static class Seed Provider = ThemeProvider.System, FileName = "dark.scss", IsDefault = true, - Description = "Default theme shipped with Kavita" } - }.ToArray() - ]; + }.ToArray()); public static readonly ImmutableArray DefaultStreams = ImmutableArray.Create( new List @@ -80,53 +76,50 @@ public static class Seed }, }.ToArray()); - public static readonly ImmutableArray DefaultSideNavStreams = ImmutableArray.Create( + public static readonly ImmutableArray DefaultSideNavStreams = ImmutableArray.Create(new[] + { new AppUserSideNavStream() - { - Name = "want-to-read", - StreamType = SideNavStreamType.WantToRead, - Order = 1, - IsProvided = true, - Visible = true - }, new AppUserSideNavStream() - { - Name = "collections", - StreamType = SideNavStreamType.Collections, - Order = 2, - IsProvided = true, - Visible = true - }, new AppUserSideNavStream() - { - Name = "reading-lists", - StreamType = SideNavStreamType.ReadingLists, - Order = 3, - IsProvided = true, - Visible = true - }, new AppUserSideNavStream() - { - Name = "bookmarks", - StreamType = SideNavStreamType.Bookmarks, - Order = 4, - IsProvided = true, - Visible = true - }, new AppUserSideNavStream() - { - Name = "all-series", - StreamType = SideNavStreamType.AllSeries, - Order = 5, - IsProvided = true, - Visible = true - }, - new AppUserSideNavStream() - { - Name = "browse-authors", - StreamType = SideNavStreamType.BrowsePeople, - Order = 6, - IsProvided = true, - Visible = true + { + Name = "want-to-read", + StreamType = SideNavStreamType.WantToRead, + Order = 1, + IsProvided = true, + Visible = true + }, + new AppUserSideNavStream() + { + Name = "collections", + StreamType = SideNavStreamType.Collections, + Order = 2, + IsProvided = true, + Visible = true + }, + new AppUserSideNavStream() + { + Name = "reading-lists", + StreamType = SideNavStreamType.ReadingLists, + Order = 3, + IsProvided = true, + Visible = true + }, + new AppUserSideNavStream() + { + Name = "bookmarks", + StreamType = SideNavStreamType.Bookmarks, + Order = 4, + IsProvided = true, + Visible = true + }, + new AppUserSideNavStream() + { + Name = "all-series", + StreamType = SideNavStreamType.AllSeries, + Order = 5, + IsProvided = true, + Visible = true + } }); - public static async Task SeedRoles(RoleManager roleManager) { var roles = typeof(PolicyConstants) @@ -193,10 +186,10 @@ public static class Seed var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams); foreach (var user in allUsers) { + if (user.SideNavStreams.Count != 0) continue; user.SideNavStreams ??= new List(); foreach (var defaultStream in DefaultSideNavStreams) { - if (user.SideNavStreams.Any(s => s.Name == defaultStream.Name && s.StreamType == defaultStream.StreamType)) continue; var newStream = new AppUserSideNavStream() { Name = defaultStream.Name, @@ -220,9 +213,8 @@ public static class Seed { new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, new() {Key = ServerSettingKey.TaskScan, Value = "daily"}, - new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, - new() {Key = ServerSettingKey.TaskCleanup, Value = "daily"}, new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, + new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, new() { Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory) @@ -236,10 +228,12 @@ public static class Seed }, // Not used from DB, but DB is sync with appSettings.json new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new() {Key = ServerSettingKey.EnableOpds, Value = "true"}, + new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, new() {Key = ServerSettingKey.BaseUrl, Value = "/"}, new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()}, new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()}, new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory}, + new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl}, new() {Key = ServerSettingKey.TotalBackups, Value = "30"}, new() {Key = ServerSettingKey.TotalLogs, Value = "30"}, new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"}, @@ -252,23 +246,11 @@ public static class Seed new() { Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty }, // Not used from DB, but DB is sync with appSettings.json - - new() {Key = ServerSettingKey.EmailHost, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailPort, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailAuthPassword, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailAuthUserName, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailSenderAddress, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailSenderDisplayName, Value = string.Empty}, - new() {Key = ServerSettingKey.EmailEnableSsl, Value = "true"}, - new() {Key = ServerSettingKey.EmailSizeLimit, Value = 26_214_400 + string.Empty}, - new() {Key = ServerSettingKey.EmailCustomizedTemplates, Value = "false"}, - new() {Key = ServerSettingKey.FirstInstallVersion, Value = BuildInfo.Version.ToString()}, - new() {Key = ServerSettingKey.FirstInstallDate, Value = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)}, }.ToArray()); foreach (var defaultSetting in DefaultSettings) { - var existing = await context.ServerSetting.FirstOrDefaultAsync(s => s.Key == defaultSetting.Key); + var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key); if (existing == null) { await context.ServerSetting.AddAsync(defaultSetting); @@ -278,51 +260,16 @@ public static class Seed await context.SaveChangesAsync(); // Port, IpAddresses and LoggingLevel are managed in appSettings.json. Update the DB values to match - (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.Port)).Value = + context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value = Configuration.Port + string.Empty; - (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.IpAddresses)).Value = + context.ServerSetting.First(s => s.Key == ServerSettingKey.IpAddresses).Value = Configuration.IpAddresses; - (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheDirectory)).Value = + context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value = directoryService.CacheDirectory + string.Empty; - (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.BackupDirectory)).Value = + context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value = DirectoryService.BackupDirectory + string.Empty; - (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheSize)).Value = + context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheSize).Value = Configuration.CacheSize + string.Empty; - - await context.SaveChangesAsync(); - } - - public static async Task SeedMetadataSettings(DataContext context) - { - await context.Database.EnsureCreatedAsync(); - - var existing = await context.MetadataSettings.FirstOrDefaultAsync(); - if (existing == null) - { - existing = new MetadataSettings() - { - Enabled = true, - EnablePeople = true, - EnableRelationships = true, - EnableSummary = true, - EnablePublicationStatus = true, - EnableStartDate = true, - EnableTags = false, - EnableGenres = true, - EnableLocalizedName = false, - FirstLastPeopleNaming = true, - EnableCoverImage = true, - EnableChapterTitle = false, - EnableChapterSummary = true, - EnableChapterPublisher = true, - EnableChapterCoverImage = false, - EnableChapterReleaseDate = true, - PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character] - }; - await context.MetadataSettings.AddAsync(existing); - } - - await context.SaveChangesAsync(); } diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index d72dd3bc7..3eadc3de0 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -2,14 +2,15 @@ using System.Threading.Tasks; using API.Data.Repositories; using API.Entities; +using API.Services; using AutoMapper; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; namespace API.Data; public interface IUnitOfWork { - DataContext DataContext { get; } ISeriesRepository SeriesRepository { get; } IUserRepository UserRepository { get; } ILibraryRepository LibraryRepository { get; } @@ -31,81 +32,49 @@ public interface IUnitOfWork IUserTableOfContentRepository UserTableOfContentRepository { get; } IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } - IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } - IEmailHistoryRepository EmailHistoryRepository { get; } - IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); Task RollbackAsync(); } - public class UnitOfWork : IUnitOfWork { private readonly DataContext _context; private readonly IMapper _mapper; private readonly UserManager _userManager; + private readonly ILocalizationService _localizationService; public UnitOfWork(DataContext context, IMapper mapper, UserManager userManager) { _context = context; _mapper = mapper; _userManager = userManager; - - SeriesRepository = new SeriesRepository(_context, _mapper, _userManager); - UserRepository = new UserRepository(_context, _userManager, _mapper); - LibraryRepository = new LibraryRepository(_context, _mapper); - VolumeRepository = new VolumeRepository(_context, _mapper); - SettingsRepository = new SettingsRepository(_context, _mapper); - AppUserProgressRepository = new AppUserProgressRepository(_context, _mapper); - CollectionTagRepository = new CollectionTagRepository(_context, _mapper); - ChapterRepository = new ChapterRepository(_context, _mapper); - ReadingListRepository = new ReadingListRepository(_context, _mapper); - SeriesMetadataRepository = new SeriesMetadataRepository(_context); - PersonRepository = new PersonRepository(_context, _mapper); - GenreRepository = new GenreRepository(_context, _mapper); - TagRepository = new TagRepository(_context, _mapper); - SiteThemeRepository = new SiteThemeRepository(_context, _mapper); - MangaFileRepository = new MangaFileRepository(_context); - DeviceRepository = new DeviceRepository(_context, _mapper); - MediaErrorRepository = new MediaErrorRepository(_context, _mapper); - ScrobbleRepository = new ScrobbleRepository(_context, _mapper); - UserTableOfContentRepository = new UserTableOfContentRepository(_context, _mapper); - AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper); - AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper); - ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper); - EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper); - AppUserReadingProfileRepository = new AppUserReadingProfileRepository(_context, _mapper); } - /// - /// This is here for Scanner only. Don't use otherwise. - /// - public DataContext DataContext => _context; - public ISeriesRepository SeriesRepository { get; } - public IUserRepository UserRepository { get; } - public ILibraryRepository LibraryRepository { get; } - public IVolumeRepository VolumeRepository { get; } - public ISettingsRepository SettingsRepository { get; } - public IAppUserProgressRepository AppUserProgressRepository { get; } - public ICollectionTagRepository CollectionTagRepository { get; } - public IChapterRepository ChapterRepository { get; } - public IReadingListRepository ReadingListRepository { get; } - public ISeriesMetadataRepository SeriesMetadataRepository { get; } - public IPersonRepository PersonRepository { get; } - public IGenreRepository GenreRepository { get; } - public ITagRepository TagRepository { get; } - public ISiteThemeRepository SiteThemeRepository { get; } - public IMangaFileRepository MangaFileRepository { get; } - public IDeviceRepository DeviceRepository { get; } - public IMediaErrorRepository MediaErrorRepository { get; } - public IScrobbleRepository ScrobbleRepository { get; } - public IUserTableOfContentRepository UserTableOfContentRepository { get; } - public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } - public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } - public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } - public IEmailHistoryRepository EmailHistoryRepository { get; } - public IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } + public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper); + public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper); + public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); + + public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper); + + public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper); + + public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context, _mapper); + public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper); + public IChapterRepository ChapterRepository => new ChapterRepository(_context, _mapper); + public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper); + public ISeriesMetadataRepository SeriesMetadataRepository => new SeriesMetadataRepository(_context); + public IPersonRepository PersonRepository => new PersonRepository(_context, _mapper); + public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper); + public ITagRepository TagRepository => new TagRepository(_context, _mapper); + public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper); + public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context); + public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper); + public IMediaErrorRepository MediaErrorRepository => new MediaErrorRepository(_context, _mapper); + public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper); + public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper); + public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper); + public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/EmailTemplates/EmailChange.html b/API/EmailTemplates/EmailChange.html deleted file mode 100644 index 6423e024f..000000000 --- a/API/EmailTemplates/EmailChange.html +++ /dev/null @@ -1,27 +0,0 @@ - - -

Email Change Update

-

Your account's email has been updated on {{InvitingUser}}'s Kavita instance.

-

Please click the following link to validate your email change. The email is not changed until you complete validation.

- - - - - - - - - - -

If the button above does not work, please find the link here: {{Link}}

- - - diff --git a/API/EmailTemplates/EmailConfirm.html b/API/EmailTemplates/EmailConfirm.html deleted file mode 100644 index 194f88ec8..000000000 --- a/API/EmailTemplates/EmailConfirm.html +++ /dev/null @@ -1,26 +0,0 @@ - - -

You've Been Invited!

-

You have been invited to {{InvitingUser}}'s Kavita instance.

-

Please click the following link to setup an account for yourself and start reading.

- - - - - - - - - -

If the button above does not work, please find the link here: {{Link}}

- - - diff --git a/API/EmailTemplates/EmailPasswordReset.html b/API/EmailTemplates/EmailPasswordReset.html deleted file mode 100644 index 2518ffc53..000000000 --- a/API/EmailTemplates/EmailPasswordReset.html +++ /dev/null @@ -1,27 +0,0 @@ - - -

Forgot your password?

-

That's okay, it happens! Click on the button below to reset your password.

- -

If you did not perform this action, ignore this email. Your account is safe.

- - - - - - - - - -

If the button above does not work, please find the link here: {{Link}}

- - - diff --git a/API/EmailTemplates/EmailTest.html b/API/EmailTemplates/EmailTest.html deleted file mode 100644 index ae00e4889..000000000 --- a/API/EmailTemplates/EmailTest.html +++ /dev/null @@ -1,8 +0,0 @@ - - -

This is a Test Email

-

Congrats! Your instance of Kavita is setup to email correctly!

- -

If you did not perform this action, ignore this email. Your account is safe.

- - diff --git a/API/EmailTemplates/KavitaPlusDebug.html b/API/EmailTemplates/KavitaPlusDebug.html deleted file mode 100644 index e165dfb98..000000000 --- a/API/EmailTemplates/KavitaPlusDebug.html +++ /dev/null @@ -1,20 +0,0 @@ - - -

A User needs manual registration

- - - -
- - - - - - -
- - diff --git a/API/EmailTemplates/SendToDevice.html b/API/EmailTemplates/SendToDevice.html deleted file mode 100644 index 673d773c5..000000000 --- a/API/EmailTemplates/SendToDevice.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

You sent file(s) from Kavita

-

Please find attached the file(s) you've sent.

- - diff --git a/API/EmailTemplates/TokenExpiration.html b/API/EmailTemplates/TokenExpiration.html deleted file mode 100644 index 1162dc75b..000000000 --- a/API/EmailTemplates/TokenExpiration.html +++ /dev/null @@ -1,28 +0,0 @@ - - -

Your {{Provider}} Token is Expired!

- - - -

Kavita will stop syncing with {{Provider}} until you renew your token.

- - - - - -
- - - - - -
- - - -

If the button above does not work, please find the link here: {{Link}}

- - diff --git a/API/EmailTemplates/TokenExpiringSoon.html b/API/EmailTemplates/TokenExpiringSoon.html deleted file mode 100644 index 960b9b6e5..000000000 --- a/API/EmailTemplates/TokenExpiringSoon.html +++ /dev/null @@ -1,28 +0,0 @@ - - -

Your {{Provider}} Token will Expire soon!

- - - -

Kavita will stop syncing with {{Provider}} until you renew your token.

- - - - - -
- - - - - -
- - - -

If the button above does not work, please find the link here: {{Link}}

- - diff --git a/API/EmailTemplates/base.html b/API/EmailTemplates/base.html deleted file mode 100644 index e06386cc7..000000000 --- a/API/EmailTemplates/base.html +++ /dev/null @@ -1,527 +0,0 @@ - - - - - - - - - - - - - - - - - - Kavita - {{Preheader}} - - - - - -
{{Preheader}}
- - - - - - - - diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 848636209..62c8cc81a 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -19,9 +19,7 @@ public class AppUser : IdentityUser, IHasConcurrencyToken public ICollection UserRoles { get; set; } = null!; public ICollection Progresses { get; set; } = null!; public ICollection Ratings { get; set; } = null!; - public ICollection ChapterRatings { get; set; } = null!; public AppUserPreferences UserPreferences { get; set; } = null!; - public ICollection ReadingProfiles { get; set; } = null!; /// /// Bookmarks associated with this User /// @@ -31,13 +29,9 @@ public class AppUser : IdentityUser, IHasConcurrencyToken ///
public ICollection ReadingLists { get; set; } = null!; /// - /// Collections associated with this user - /// - public ICollection Collections { get; set; } = null!; - /// /// A list of Series the user want's to read /// - public ICollection WantToRead { get; set; } = null!; + public ICollection WantToRead { get; set; } = null!; /// /// A list of Devices which allows the user to send files to /// @@ -69,27 +63,6 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// Requires Kavita+ Subscription public string? AniListAccessToken { get; set; } - /// - /// The Username of the MAL user - /// - public string? MalUserName { get; set; } - /// - /// The Client ID for the user's MAL account. User should create a client on MAL for this. - /// - public string? MalAccessToken { get; set; } - - /// - /// Has the user ran Scrobble Event Generation - /// - /// Only applicable for Kavita+ and when a Token is present - public bool HasRunScrobbleEventGeneration { get; set; } - /// - /// The timestamp of when Scrobble Event Generation ran (Utc) - /// - /// Kavita+ only - public DateTime ScrobbleEventGenerationRan { get; set; } - - /// /// A list of Series the user doesn't want scrobbling for /// diff --git a/API/Entities/AppUserChapterRating.cs b/API/Entities/AppUserChapterRating.cs deleted file mode 100644 index a78096bda..000000000 --- a/API/Entities/AppUserChapterRating.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace API.Entities; - -public class AppUserChapterRating -{ - public int Id { get; set; } - /// - /// A number between 0-5.0 that represents how good a series is. - /// - public float Rating { get; set; } - /// - /// If the rating has been explicitly set. Otherwise, the 0.0 rating should be ignored as it's not rated - /// - public bool HasBeenRated { get; set; } - /// - /// A short summary the user can write when giving their review. - /// - public string? Review { get; set; } - /// - /// An optional tagline for the review - /// - public int SeriesId { get; set; } - public Series Series { get; set; } = null!; - - public int ChapterId { get; set; } - public Chapter Chapter { get; set; } = null!; - - // Relationships - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } = null!; -} diff --git a/API/Entities/AppUserCollection.cs b/API/Entities/AppUserCollection.cs deleted file mode 100644 index 2a6d8faff..000000000 --- a/API/Entities/AppUserCollection.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; - - -namespace API.Entities; - -/// -/// Represents a Collection of Series for a given User -/// -public class AppUserCollection : IEntityDate, IHasCoverImage -{ - public int Id { get; set; } - public required string Title { get; set; } - /// - /// A normalized string used to check if the collection already exists in the DB - /// - public required string NormalizedTitle { get; set; } - public string? Summary { get; set; } - /// - /// Reading lists that are promoted are only done by admins - /// - public bool Promoted { get; set; } - public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } - public bool CoverImageLocked { get; set; } - /// - /// The highest age rating from all Series within the collection - /// - public required AgeRating AgeRating { get; set; } = AgeRating.Unknown; - public ICollection Items { get; set; } = null!; - public DateTime Created { get; set; } - public DateTime LastModified { get; set; } - public DateTime CreatedUtc { get; set; } - public DateTime LastModifiedUtc { get; set; } - - // Sync stuff for Kavita+ - /// - /// Last time Kavita Synced the Collection with an upstream source (for non Kavita sourced collections) - /// - public DateTime LastSyncUtc { get; set; } - /// - /// Who created/manages the list. Non-Kavita lists are not editable by the user, except to promote - /// - public ScrobbleProvider Source { get; set; } = ScrobbleProvider.Kavita; - /// - /// For Non-Kavita sourced collections, the url to sync from - /// - public string? SourceUrl { get; set; } - /// - /// Total number of items as of the last sync. Not applicable for Kavita managed collections. - /// - public int TotalSourceCount { get; set; } - /// - /// A
separated string of all missing series - ///
- public string? MissingSeriesFromSource { get; set; } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } - - // Relationship - public AppUser AppUser { get; set; } = null!; - public int AppUserId { get; set; } -} diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index b0f21bcba..640ecc1ea 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using API.Data; +using API.Data; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -8,9 +7,6 @@ namespace API.Entities; public class AppUserPreferences { public int Id { get; set; } - - #region MangaReader - /// /// Manga Reader Option: What direction should the next/prev page buttons go /// @@ -55,15 +51,6 @@ public class AppUserPreferences /// Manga Reader Option: Should swiping trigger pagination ///
public bool SwipeToPaginate { get; set; } - /// - /// Manga Reader Option: Allow Automatic Webtoon detection - /// - public bool AllowAutomaticWebtoonReaderDetection { get; set; } - - #endregion - - #region EpubReader - /// /// Book Reader Option: Override extra Margin /// @@ -88,11 +75,17 @@ public class AppUserPreferences /// Book Reader Option: What direction should the next/prev page buttons go ///
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; + /// /// Book Reader Option: Defines the writing styles vertical/horizontal /// public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal; /// + /// UI Site Global Setting: The UI theme the user should use. + /// + /// Should default to Dark + public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0]; + /// /// Book Reader Option: The color theme to decorate the book contents /// /// Should default to Dark @@ -108,33 +101,6 @@ public class AppUserPreferences ///
/// Defaults to false public bool BookReaderImmersiveMode { get; set; } = false; - #endregion - - #region PdfReader - - /// - /// PDF Reader: Theme of the Reader - /// - public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - /// - /// PDF Reader: Scroll mode of the reader - /// - public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - /// - /// PDF Reader: Spread Mode of the reader - /// - public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - - - #endregion - - #region Global - - /// - /// UI Site Global Setting: The UI theme the user should use. - /// - /// Should default to Dark - public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0]; /// /// Global Site Option: If the UI should layout items as Cards or List items /// @@ -165,18 +131,6 @@ public class AppUserPreferences /// UI Site Global Setting: The language locale that should be used for the user ///
public string Locale { get; set; } - #endregion - - #region KavitaPlus - /// - /// Should this account have Scrobbling enabled for AniList - /// - public bool AniListScrobblingEnabled { get; set; } - /// - /// Should this account have Want to Read Sync enabled - /// - public bool WantToReadSync { get; set; } - #endregion public AppUser AppUser { get; set; } = null!; public int AppUserId { get; set; } diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index beaf07220..c972af78a 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -59,10 +59,4 @@ public class AppUserProgress : IEntityDate /// User this progress belongs to ///
public int AppUserId { get; set; } - - public void MarkModified() - { - LastModified = DateTime.Now; - LastModifiedUtc = DateTime.UtcNow; - } } diff --git a/API/Entities/AppUserRating.cs b/API/Entities/AppUserRating.cs index e76838926..91734b445 100644 --- a/API/Entities/AppUserRating.cs +++ b/API/Entities/AppUserRating.cs @@ -1,6 +1,4 @@  -using System; - namespace API.Entities; #nullable enable public class AppUserRating @@ -11,7 +9,7 @@ public class AppUserRating ///
public float Rating { get; set; } /// - /// If the rating has been explicitly set. Otherwise, the 0.0 rating should be ignored as it's not rated + /// If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated /// public bool HasBeenRated { get; set; } /// @@ -21,11 +19,11 @@ public class AppUserRating /// /// An optional tagline for the review /// - [Obsolete("No longer used")] public string? Tagline { get; set; } public int SeriesId { get; set; } public Series Series { get; set; } = null!; + // Relationships public int AppUserId { get; set; } public AppUser AppUser { get; set; } = null!; diff --git a/API/Entities/AppUserReadingProfile.cs b/API/Entities/AppUserReadingProfile.cs deleted file mode 100644 index 9b238b4f5..000000000 --- a/API/Entities/AppUserReadingProfile.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; - -namespace API.Entities; - -public enum BreakPoint -{ - [Description("Never")] - Never = 0, - [Description("Mobile")] - Mobile = 1, - [Description("Tablet")] - Tablet = 2, - [Description("Desktop")] - Desktop = 3, -} - -public class AppUserReadingProfile -{ - public int Id { get; set; } - - public string Name { get; set; } - public string NormalizedName { get; set; } - - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } - - public ReadingProfileKind Kind { get; set; } - public List LibraryIds { get; set; } - public List SeriesIds { get; set; } - - #region MangaReader - - /// - /// Manga Reader Option: What direction should the next/prev page buttons go - /// - public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; - /// - /// Manga Reader Option: How should the image be scaled to screen - /// - public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic; - /// - /// Manga Reader Option: Which side of a split image should we show first - /// - public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.FitSplit; - /// - /// Manga Reader Option: How the manga reader should perform paging or reading of the file - /// - /// Webtoon uses scrolling to page, MANGA_LR uses paging by clicking left/right side of reader, MANGA_UD uses paging - /// by clicking top/bottom sides of reader. - /// - /// - public ReaderMode ReaderMode { get; set; } - /// - /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction - /// - public bool AutoCloseMenu { get; set; } = true; - /// - /// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change - /// - public bool ShowScreenHints { get; set; } = true; - /// - /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages - /// - public bool EmulateBook { get; set; } = false; - /// - /// Manga Reader Option: How many pages to display in the reader at once - /// - public LayoutMode LayoutMode { get; set; } = LayoutMode.Single; - /// - /// Manga Reader Option: Background color of the reader - /// - public string BackgroundColor { get; set; } = "#000000"; - /// - /// Manga Reader Option: Should swiping trigger pagination - /// - public bool SwipeToPaginate { get; set; } - /// - /// Manga Reader Option: Allow Automatic Webtoon detection - /// - public bool AllowAutomaticWebtoonReaderDetection { get; set; } - /// - /// Manga Reader Option: Optional fixed width override - /// - public int? WidthOverride { get; set; } = null; - /// - /// Manga Reader Option: Disable the width override if the screen is past the breakpoint - /// - public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; - - #endregion - - #region EpubReader - - /// - /// Book Reader Option: Override extra Margin - /// - public int BookReaderMargin { get; set; } = 15; - /// - /// Book Reader Option: Override line-height - /// - public int BookReaderLineSpacing { get; set; } = 100; - /// - /// Book Reader Option: Override font size - /// - public int BookReaderFontSize { get; set; } = 100; - /// - /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override - /// - public string BookReaderFontFamily { get; set; } = "default"; - /// - /// Book Reader Option: Allows tapping on side of screens to paginate - /// - public bool BookReaderTapToPaginate { get; set; } = false; - /// - /// Book Reader Option: What direction should the next/prev page buttons go - /// - public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; - /// - /// Book Reader Option: Defines the writing styles vertical/horizontal - /// - public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal; - /// - /// Book Reader Option: The color theme to decorate the book contents - /// - /// Should default to Dark - public string BookThemeName { get; set; } = "Dark"; - /// - /// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height, - /// 2 column is fit to height, 2 columns - /// - /// Defaults to Default - public BookPageLayoutMode BookReaderLayoutMode { get; set; } = BookPageLayoutMode.Default; - /// - /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. - /// - /// Defaults to false - public bool BookReaderImmersiveMode { get; set; } = false; - #endregion - - #region PdfReader - - /// - /// PDF Reader: Theme of the Reader - /// - public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - /// - /// PDF Reader: Scroll mode of the reader - /// - public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - /// - /// PDF Reader: Spread Mode of the reader - /// - public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - - - #endregion -} diff --git a/API/Entities/AppUserWantToRead.cs b/API/Entities/AppUserWantToRead.cs deleted file mode 100644 index d41e44962..000000000 --- a/API/Entities/AppUserWantToRead.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace API.Entities; - -public class AppUserWantToRead -{ - public int Id { get; set; } - - public required int SeriesId { get; set; } - public virtual Series Series { get; set; } - - - // Relationships - /// - /// Navigational Property for EF. Links to a unique AppUser - /// - public AppUser AppUser { get; set; } = null!; - /// - /// User this table of content belongs to - /// - public int AppUserId { get; set; } -} diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index fe3646943..ffb88cafb 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -1,45 +1,23 @@ using System; using System.Collections.Generic; -using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Extensions; using API.Services.Tasks.Scanner.Parser; namespace API.Entities; -public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata +public class Chapter : IEntityDate, IHasReadTimeEstimate { public int Id { get; set; } /// - /// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If the chapter is a special, will return the Special Name + /// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// public required string Range { get; set; } /// /// Smallest number of the Range. Can be a partial like Chapter 4.5 /// - [Obsolete("Use MinNumber and MaxNumber instead")] public required string Number { get; set; } /// - /// Minimum Chapter Number. - /// - public float MinNumber { get; set; } - /// - /// Maximum Chapter Number - /// - public float MaxNumber { get; set; } - /// - /// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden. - /// - public float SortOrder { get; set; } - /// - /// Can the sort order be updated on scan or is it locked from UI - /// - public bool SortOrderLocked { get; set; } - /// /// The files that represent this Chapter /// public ICollection Files { get; set; } = null!; @@ -48,9 +26,11 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } + /// + /// Relative path to the (managed) image file representing the cover image + /// + /// The file is managed internally to Kavita's APPDIR public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } public bool CoverImageLocked { get; set; } /// /// Total number of pages in all MangaFiles @@ -64,7 +44,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP /// Used for books/specials to display custom title. For non-specials/books, will be set to /// public string? Title { get; set; } - /// /// Age Rating for the issue/chapter /// @@ -120,59 +99,22 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP /// public int MaxHoursToRead { get; set; } /// - public float AvgHoursToRead { get; set; } + public int AvgHoursToRead { get; set; } /// /// Comma-separated link of urls to external services that have some relation to the Chapter /// public string WebLinks { get; set; } = string.Empty; public string ISBN { get; set; } = string.Empty; - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } = []; - - /// - /// (Kavita+) Average rating from Kavita+ metadata - /// - public float AverageExternalRating { get; set; } = 0f; - - #region Locks - - public bool AgeRatingLocked { get; set; } - public bool TitleNameLocked { get; set; } - public bool GenresLocked { get; set; } - public bool TagsLocked { get; set; } - public bool WriterLocked { get; set; } - public bool CharacterLocked { get; set; } - public bool ColoristLocked { get; set; } - public bool EditorLocked { get; set; } - public bool InkerLocked { get; set; } - public bool ImprintLocked { get; set; } - public bool LettererLocked { get; set; } - public bool PencillerLocked { get; set; } - public bool PublisherLocked { get; set; } - public bool TranslatorLocked { get; set; } - public bool TeamLocked { get; set; } - public bool LocationLocked { get; set; } - public bool CoverArtistLocked { get; set; } - public bool LanguageLocked { get; set; } - public bool SummaryLocked { get; set; } - public bool ISBNLocked { get; set; } - public bool ReleaseDateLocked { get; set; } - - #endregion - /// /// All people attached at a Chapter level. Usually Comics will have different people per issue. /// - public ICollection People { get; set; } = new List(); + public ICollection People { get; set; } = new List(); /// /// Genres for the Chapter /// public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); - public ICollection Ratings { get; set; } = []; public ICollection UserProgress { get; set; } @@ -181,92 +123,17 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public Volume Volume { get; set; } = null!; public int VolumeId { get; set; } - public ICollection ExternalReviews { get; set; } = []; - public ICollection ExternalRatings { get; set; } = null!; - public void UpdateFrom(ParserInfo info) { Files ??= new List(); IsSpecial = info.IsSpecialInfo(); if (IsSpecial) { - Number = Parser.DefaultChapter; - MinNumber = Parser.DefaultChapterNumber; - MaxNumber = Parser.DefaultChapterNumber; + Number = "0"; } - Title = (IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) + Title = (IsSpecial && info.Format == MangaFormat.Epub) ? info.Title - : Parser.RemoveExtensionIfSupported(Range); + : Range; - var specialTreatment = info.IsSpecialInfo(); - Range = specialTreatment ? info.Filename : info.Chapters; - } - - /// - /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. - /// - /// - public string GetNumberTitle() - { - try - { - if (MinNumber.Is(MaxNumber)) - { - if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial) - { - return Parser.RemoveExtensionIfSupported(Title); - } - - if (MinNumber.Is(0f) && !float.TryParse(Range, CultureInfo.InvariantCulture, out _)) - { - return $"{Range.ToString(CultureInfo.InvariantCulture)}"; - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}"; - - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}-{MaxNumber.ToString(CultureInfo.InvariantCulture)}"; - } - catch (Exception) - { - return MinNumber.ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special - /// - /// - public bool IsSingleVolumeChapter() - { - return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial; - } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } - - public bool IsPersonRoleLocked(PersonRole role) - { - return role switch - { - PersonRole.Character => CharacterLocked, - PersonRole.Writer => WriterLocked, - PersonRole.Penciller => PencillerLocked, - PersonRole.Inker => InkerLocked, - PersonRole.Colorist => ColoristLocked, - PersonRole.Letterer => LettererLocked, - PersonRole.CoverArtist => CoverArtistLocked, - PersonRole.Editor => EditorLocked, - PersonRole.Publisher => PublisherLocked, - PersonRole.Translator => TranslatorLocked, - PersonRole.Imprint => ImprintLocked, - PersonRole.Team => TeamLocked, - PersonRole.Location => LocationLocked, - _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) - }; } } diff --git a/API/Entities/CollectionTag.cs b/API/Entities/CollectionTag.cs index 5370176de..2594a9772 100644 --- a/API/Entities/CollectionTag.cs +++ b/API/Entities/CollectionTag.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using API.Entities.Metadata; -using API.Services.Plus; using Microsoft.EntityFrameworkCore; namespace API.Entities; @@ -9,7 +7,6 @@ namespace API.Entities; /// /// Represents a user entered field that is used as a tagging and grouping mechanism /// -[Obsolete("Use AppUserCollection instead")] [Index(nameof(Id), nameof(Promoted), IsUnique = true)] public class CollectionTag { @@ -44,21 +41,6 @@ public class CollectionTag public ICollection SeriesMetadatas { get; set; } = null!; - /// - /// Is this Collection tag managed by another system, like Kavita+ - /// - //public bool IsManaged { get; set; } = false; - - /// - /// The last time this Collection was Synchronized. Only applicable for Managed Tags. - /// - //public DateTime LastSynchronized { get; set; } - - /// - /// Who created this Collection (Kavita, or external services) - /// - //public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita; - /// /// Not Used due to not using concurrency update /// diff --git a/API/Entities/EmailHistory.cs b/API/Entities/EmailHistory.cs deleted file mode 100644 index f1ab95ca5..000000000 --- a/API/Entities/EmailHistory.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using API.Entities.Interfaces; -using Microsoft.EntityFrameworkCore; - -namespace API.Entities; - -/// -/// Records all emails that are sent from Kavita -/// -[Index("Sent", "AppUserId", "EmailTemplate", "SendDate")] -public class EmailHistory : IEntityDate -{ - public long Id { get; set; } - public bool Sent { get; set; } - public DateTime SendDate { get; set; } = DateTime.UtcNow; - public string EmailTemplate { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - - public string DeliveryStatus { get; set; } - public string ErrorMessage { get; set; } - - public int AppUserId { get; set; } - public virtual AppUser AppUser { get; set; } - - - public DateTime Created { get; set; } - public DateTime CreatedUtc { get; set; } - public DateTime LastModified { get; set; } - public DateTime LastModifiedUtc { get; set; } -} diff --git a/API/Entities/Enums/FileTypeGroup.cs b/API/Entities/Enums/FileTypeGroup.cs deleted file mode 100644 index eda039fc9..000000000 --- a/API/Entities/Enums/FileTypeGroup.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums; - -/// -/// Represents a set of file types that can be scanned -/// -public enum FileTypeGroup -{ - [Description("Archive")] - Archive = 1, - [Description("EPub")] - Epub = 2, - [Description("Pdf")] - Pdf = 3, - [Description("Images")] - Images = 4 -} diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index a8d943b2d..5f4ab1cc7 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -12,26 +12,11 @@ public enum LibraryType /// /// Uses Comic regex for filename parsing /// - [Description("Comic (Legacy)")] + [Description("Comic")] Comic = 1, /// /// Uses Manga regex for filename parsing also uses epub metadata /// [Description("Book")] Book = 2, - /// - /// Uses a different type of grouping and parsing mechanism - /// - [Description("Image")] - Image = 3, - /// - /// Allows Books to Scrobble with AniList for Kavita+ - /// - [Description("Light Novel")] - LightNovel = 4, - /// - /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing - /// - [Description("Comic")] - ComicVine = 5, } diff --git a/API/Entities/Enums/MetadataFieldType.cs b/API/Entities/Enums/MetadataFieldType.cs deleted file mode 100644 index 0052b6599..000000000 --- a/API/Entities/Enums/MetadataFieldType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace API.Entities.Enums; - -public enum MetadataFieldType -{ - Genre = 0, - Tag = 1, -} diff --git a/API/Entities/Enums/PersonRole.cs b/API/Entities/Enums/PersonRole.cs index f7ad45021..bd84985c0 100644 --- a/API/Entities/Enums/PersonRole.cs +++ b/API/Entities/Enums/PersonRole.cs @@ -24,11 +24,7 @@ public enum PersonRole /// /// The Translator /// - Translator = 12, - /// - /// The publisher before another Publisher bought - /// - Imprint = 13, - Team = 14, - Location = 15 + Translator = 12 + + } diff --git a/API/Entities/Enums/RatingAuthority.cs b/API/Entities/Enums/RatingAuthority.cs deleted file mode 100644 index 0f358a9a7..000000000 --- a/API/Entities/Enums/RatingAuthority.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums; - -public enum RatingAuthority -{ - /// - /// Rating was from a User (internet or local) - /// - [Description("User")] - User = 0, - /// - /// Rating was from Professional Critics - /// - [Description("Critic")] - Critic = 1, -} diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/API/Entities/Enums/ReadingProfileKind.cs deleted file mode 100644 index 0f9cfa20b..000000000 --- a/API/Entities/Enums/ReadingProfileKind.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace API.Entities.Enums; - -public enum ReadingProfileKind -{ - /// - /// Generate by Kavita when registering a user, this is your default profile - /// - Default, - /// - /// Created by the user in the UI or via the API - /// - User, - /// - /// Automatically generated by Kavita to track changes made in the readers. Can be converted to a User Reading Profile. - /// - Implicit -} diff --git a/API/Entities/Enums/RelationKind.cs b/API/Entities/Enums/RelationKind.cs index 61516ec0d..aa10e6816 100644 --- a/API/Entities/Enums/RelationKind.cs +++ b/API/Entities/Enums/RelationKind.cs @@ -71,11 +71,6 @@ public enum RelationKind /// Same story, could be translation, colorization... Different edition of the series /// [Description("Edition")] - Edition = 13, - /// - /// The target series is an annual of the Series - /// - [Description("Annual")] - Annual = 14 + Edition = 13 } diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index b1050d553..af699a3d9 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -52,7 +52,6 @@ public enum ServerSettingKey /// Is Authentication needed for non-admin accounts ///
/// Deprecated. This is no longer used v0.5.1+. Assume Authentication is always in effect - [Obsolete("Not supported as of v0.5.1")] [Description("EnableAuthentication")] EnableAuthentication = 8, /// @@ -80,7 +79,6 @@ public enum ServerSettingKey /// If SMTP is enabled on the server /// [Description("CustomEmailService")] - [Obsolete("Use Email settings instead")] EmailServiceUrl = 13, /// /// If Kavita should save bookmarks as WebP images @@ -149,52 +147,6 @@ public enum ServerSettingKey /// The size of the cover image thumbnail. Defaults to .Default /// [Description("CoverImageSize")] - CoverImageSize = 27, - #region EmailSettings - /// - /// The address of the emailer host - /// - [Description("EmailSenderAddress")] - EmailSenderAddress = 28, - /// - /// What the email name should be - /// - [Description("EmailSenderDisplayName")] - EmailSenderDisplayName = 29, - [Description("EmailAuthUserName")] - EmailAuthUserName = 30, - [Description("EmailAuthPassword")] - EmailAuthPassword = 31, - [Description("EmailHost")] - EmailHost = 32, - [Description("EmailPort")] - EmailPort = 33, - [Description("EmailEnableSsl")] - EmailEnableSsl = 34, - /// - /// Number of bytes that the sender allows to be sent through - /// - [Description("EmailSizeLimit")] - EmailSizeLimit = 35, - /// - /// Should Kavita use config/templates for Email templates or the default ones - /// - [Description("EmailCustomizedTemplates")] - EmailCustomizedTemplates = 36, - #endregion - /// - /// When the cleanup task should run - Critical to keeping Kavita working - /// - [Description("TaskCleanup")] - TaskCleanup = 37, - /// - /// The Date Kavita was first installed - /// - [Description("FirstInstallDate")] - FirstInstallDate = 38, - /// - /// The Version of Kavita on the first run - /// - [Description("FirstInstallVersion")] - FirstInstallVersion = 39, + CoverImageSize = 27 + } diff --git a/API/Entities/Enums/Theme/ThemeProvider.cs b/API/Entities/Enums/Theme/ThemeProvider.cs index cc12a552e..45af2d94b 100644 --- a/API/Entities/Enums/Theme/ThemeProvider.cs +++ b/API/Entities/Enums/Theme/ThemeProvider.cs @@ -10,8 +10,8 @@ public enum ThemeProvider [Description("System")] System = 1, /// - /// Theme is provided by the User (ie it's custom) or Downloaded via Themes Repo + /// Theme is provided by the User (ie it's custom) /// - [Description("Custom")] - Custom = 2, + [Description("User")] + User = 2 } diff --git a/API/Entities/Enums/UserPreferences/PdfBookMode.cs b/API/Entities/Enums/UserPreferences/PdfBookMode.cs deleted file mode 100644 index 5946e17c5..000000000 --- a/API/Entities/Enums/UserPreferences/PdfBookMode.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums.UserPreferences; - -public enum PdfLayoutMode -{ - /// - /// Multiple pages render stacked (normal pdf experience) - /// - [Description("Multiple")] - Multiple = 0, - // [Description("Single")] - // Single = 1, - /// - /// A book mode where page turns are animated and layout is side-by-side - /// - [Description("Book")] - Book = 2, - // [Description("Infinite Scroll")] - // InfiniteScroll = 3 -} diff --git a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs b/API/Entities/Enums/UserPreferences/PdfScrollMode.cs deleted file mode 100644 index 93cc5bd2e..000000000 --- a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums.UserPreferences; - -/// -/// Enum values match PdfViewer's enums -/// -public enum PdfScrollMode -{ - [Description("Vertical")] - Vertical = 0, - [Description("Horizontal")] - Horizontal = 1, - // [Description("Wrapped")] - // Wrapped = 2, - /// - /// Single page view (tap to pagninate) - /// - [Description("Page")] - Page = 3 -} diff --git a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs b/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs deleted file mode 100644 index 412239d4a..000000000 --- a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums.UserPreferences; - -public enum PdfSpreadMode -{ - [Description("None")] - None = 0, - [Description("Odd")] - Odd = 1, - [Description("Even")] - Even = 2 -} diff --git a/API/Entities/Enums/UserPreferences/PdfTheme.cs b/API/Entities/Enums/UserPreferences/PdfTheme.cs deleted file mode 100644 index 0efe1dfde..000000000 --- a/API/Entities/Enums/UserPreferences/PdfTheme.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.ComponentModel; - -namespace API.Entities.Enums.UserPreferences; - -public enum PdfTheme -{ - [Description("Dark")] - Dark = 0, - [Description("Light")] - Light = 1 -} diff --git a/API/Entities/History/KavitaPlusHistory.cs b/API/Entities/History/KavitaPlusHistory.cs deleted file mode 100644 index 81b7e5e40..000000000 --- a/API/Entities/History/KavitaPlusHistory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace API.Entities.History; - -/// -/// Records history of actions Kavita+ takes -/// -// public class KavitaPlusHistory -// { -// -// } diff --git a/API/Entities/History/ManualMigrationHistory.cs b/API/Entities/History/ManualMigrationHistory.cs deleted file mode 100644 index 2f407ca1d..000000000 --- a/API/Entities/History/ManualMigrationHistory.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace API.Entities.History; - -/// -/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed -/// -public class ManualMigrationHistory -{ - public int Id { get; set; } - public string ProductVersion { get; set; } - public required string Name { get; set; } - public DateTime RanAt { get; set; } -} diff --git a/API/Entities/Interfaces/IHasCoverImage.cs b/API/Entities/Interfaces/IHasCoverImage.cs deleted file mode 100644 index 5570e37eb..000000000 --- a/API/Entities/Interfaces/IHasCoverImage.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace API.Entities.Interfaces; - -#nullable enable - -public interface IHasCoverImage -{ - /// - /// Absolute path to the (managed) image file - /// - /// The file is managed internally to Kavita's APPDIR - public string? CoverImage { get; set; } - - /// - /// Primary color derived from the Cover Image - /// - public string? PrimaryColor { get; set; } - /// - /// Secondary color derived from the Cover Image - /// - public string? SecondaryColor { get; set; } - - /// - /// Nulls out the ColorScape properties - /// - void ResetColorScape(); -} diff --git a/API/Entities/Interfaces/IHasKPlusMetadata.cs b/API/Entities/Interfaces/IHasKPlusMetadata.cs deleted file mode 100644 index 062afd7e1..000000000 --- a/API/Entities/Interfaces/IHasKPlusMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using API.Entities.MetadataMatching; - -namespace API.Entities.Interfaces; - -public interface IHasKPlusMetadata -{ - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } -} diff --git a/API/Entities/Interfaces/IHasReadTimeEstimate.cs b/API/Entities/Interfaces/IHasReadTimeEstimate.cs index aeb6f6f76..a13c43277 100644 --- a/API/Entities/Interfaces/IHasReadTimeEstimate.cs +++ b/API/Entities/Interfaces/IHasReadTimeEstimate.cs @@ -21,5 +21,5 @@ public interface IHasReadTimeEstimate /// Average hours to read the chapter ///
/// Uses a fixed number to calculate from - public float AvgHoursToRead { get; set; } + public int AvgHoursToRead { get; set; } } diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 4a48fed99..af15d06f6 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -2,16 +2,15 @@ using System.Collections.Generic; using API.Entities.Enums; using API.Entities.Interfaces; +using Microsoft.EntityFrameworkCore; namespace API.Entities; -public class Library : IEntityDate, IHasCoverImage +public class Library : IEntityDate { public int Id { get; set; } public required string Name { get; set; } public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } public LibraryType Type { get; set; } /// /// If Folder Watching is enabled for this library @@ -40,24 +39,8 @@ public class Library : IEntityDate, IHasCoverImage /// /// Should this library allow Scrobble events to emit from it /// - /// Requires a valid LicenseKey + /// Scrobbling requires a valid LicenseKey public bool AllowScrobbling { get; set; } = true; - /// - /// Allow any series within this Library to download metadata. - /// - /// This does not exclude the library from being linked to wrt Series Relationships - /// Requires a valid LicenseKey - public bool AllowMetadataMatching { get; set; } = true; - /// - /// Should Kavita read metadata files from the library - /// - public bool EnableMetadata { get; set; } = true; - /// - /// Should Kavita remove sort articles "The" for the sort name - /// - public bool RemovePrefixForSortName { get; set; } = false; - - public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -71,8 +54,6 @@ public class Library : IEntityDate, IHasCoverImage public ICollection Folders { get; set; } = null!; public ICollection AppUsers { get; set; } = null!; public ICollection Series { get; set; } = null!; - public ICollection LibraryFileTypes { get; set; } = new List(); - public ICollection LibraryExcludePatterns { get; set; } = new List(); public void UpdateLastModified() { @@ -91,10 +72,4 @@ public class Library : IEntityDate, IHasCoverImage LastScanned = (DateTime) time; } } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } } diff --git a/API/Entities/LibraryExcludedGlob.cs b/API/Entities/LibraryExcludedGlob.cs deleted file mode 100644 index 69bc86342..000000000 --- a/API/Entities/LibraryExcludedGlob.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace API.Entities; - -public class LibraryExcludePattern -{ - public int Id { get; set; } - public string Pattern { get; set; } - - public int LibraryId { get; set; } - public Library Library { get; set; } = null!; -} diff --git a/API/Entities/LibraryFileTypeGroup.cs b/API/Entities/LibraryFileTypeGroup.cs deleted file mode 100644 index a3af30d80..000000000 --- a/API/Entities/LibraryFileTypeGroup.cs +++ /dev/null @@ -1,12 +0,0 @@ -using API.Entities.Enums; - -namespace API.Entities; - -public class LibraryFileTypeGroup -{ - public int Id { get; set; } - public FileTypeGroup FileTypeGroup { get; set; } - - public int LibraryId { get; set; } - public Library Library { get; set; } = null!; -} diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index afcb23e97..14a64fc26 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -13,19 +13,10 @@ public class MangaFile : IEntityDate { public int Id { get; set; } /// - /// The filename without extension - /// - public string FileName { get; set; } - /// /// Absolute path to the archive file /// public required string FilePath { get; set; } /// - /// A hash of the document using Koreader's unique hashing algorithm - /// - /// KoreaderHash is only available for epub types - public string? KoreaderHash { get; set; } - /// /// Number of pages for the given file /// public int Pages { get; set; } diff --git a/API/Entities/Metadata/ExternalRating.cs b/API/Entities/Metadata/ExternalRating.cs deleted file mode 100644 index 7fc2b9353..000000000 --- a/API/Entities/Metadata/ExternalRating.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; - -namespace API.Entities.Metadata; - -public class ExternalRating -{ - public int Id { get; set; } - - public int AverageScore { get; set; } - public int FavoriteCount { get; set; } - public ScrobbleProvider Provider { get; set; } - /// - /// Where this rating comes from: Critic or User - /// - public RatingAuthority Authority { get; set; } = RatingAuthority.User; - public string? ProviderUrl { get; set; } - public int SeriesId { get; set; } - /// - /// This can be null when for a series-rating - /// - public int? ChapterId { get; set; } - - public ICollection ExternalSeriesMetadatas { get; set; } = null!; -} diff --git a/API/Entities/Metadata/ExternalRecommendation.cs b/API/Entities/Metadata/ExternalRecommendation.cs deleted file mode 100644 index c5bb98f20..000000000 --- a/API/Entities/Metadata/ExternalRecommendation.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using API.Services.Plus; -using Microsoft.EntityFrameworkCore; - - -namespace API.Entities.Metadata; - -[Index(nameof(SeriesId), IsUnique = false)] -public class ExternalRecommendation -{ - public int Id { get; set; } - - public required string Name { get; set; } - public required string CoverUrl { get; set; } - public required string Url { get; set; } - public string? Summary { get; set; } - public int? AniListId { get; set; } - public long? MalId { get; set; } - public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList; - - /// - /// When null, represents an external series. When set, it is a Series - /// - public int? SeriesId { get; set; } - //public virtual Series? Series { get; set; } - - // Relationships - public ICollection ExternalSeriesMetadatas { get; set; } = null!; -} diff --git a/API/Entities/Metadata/ExternalReview.cs b/API/Entities/Metadata/ExternalReview.cs deleted file mode 100644 index 73c71e5ee..000000000 --- a/API/Entities/Metadata/ExternalReview.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; - -namespace API.Entities.Metadata; - -/// -/// Represents an Externally supplied Review for a given Series -/// -public class ExternalReview -{ - public int Id { get; set; } - public string Tagline { get; set; } - public required string Body { get; set; } - /// - /// Pure text version of the body - /// - public required string BodyJustText { get; set; } - /// - /// Raw from the provider. Usually Markdown - /// - public string RawBody { get; set; } - public required ScrobbleProvider Provider { get; set; } - public RatingAuthority Authority { get; set; } = RatingAuthority.User; - public string SiteUrl { get; set; } - /// - /// Reviewer's username - /// - public string Username { get; set; } - /// - /// An Optional Rating coming from the Review - /// - public int Rating { get; set; } = 0; - /// - /// The media's overall Score - /// - public int Score { get; set; } - public int TotalVotes { get; set; } - - - public int SeriesId { get; set; } - public int? ChapterId { get; set; } - - // Relationships - public ICollection ExternalSeriesMetadatas { get; set; } = null!; -} diff --git a/API/Entities/Metadata/ExternalSeriesMetadata.cs b/API/Entities/Metadata/ExternalSeriesMetadata.cs deleted file mode 100644 index 1ab37ba3c..000000000 --- a/API/Entities/Metadata/ExternalSeriesMetadata.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace API.Entities.Metadata; - -/// -/// External Metadata from Kavita+ for a Series -/// -public class ExternalSeriesMetadata -{ - public int Id { get; set; } - /// - /// External Reviews for the Series. Managed by Kavita for Kavita+ users - /// - public ICollection ExternalReviews { get; set; } = null!; - public ICollection ExternalRatings { get; set; } = null!; - /// - /// External recommendations will include all recommendations and will have a seriesId if it's on this Kavita instance. - /// - /// Cleanup Service will perform matching to tie new series with recommendations - public ICollection ExternalRecommendations { get; set; } = null!; - - /// - /// Average External Rating. -1 means not set, 0 - 100 - /// - public int AverageExternalRating { get; set; } = -1; - - public int AniListId { get; set; } - public int CbrId { get; set; } - public long MalId { get; set; } - public string GoogleBooksId { get; set; } - - /// - /// Data is valid until this time - /// - public DateTime ValidUntilUtc { get; set; } - - public Series Series { get; set; } = null!; - public int SeriesId { get; set; } -} diff --git a/API/Entities/Metadata/SeriesBlacklist.cs b/API/Entities/Metadata/SeriesBlacklist.cs deleted file mode 100644 index 3d262eeb4..000000000 --- a/API/Entities/Metadata/SeriesBlacklist.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace API.Entities.Metadata; - -/// -/// A blacklist of Series for Kavita+ -/// -[Obsolete("Kavita v0.8.5 moved the implementation to Series.IsBlacklisted")] -public class SeriesBlacklist -{ - public int Id { get; set; } - public DateTime LastChecked { get; set; } = DateTime.UtcNow; - - public int SeriesId { get; set; } - public Series Series { get; set; } -} diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 8bb33fdc0..f3ccebc93 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -1,22 +1,28 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using API.Entities.Enums; using API.Entities.Interfaces; -using API.Entities.MetadataMatching; -using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Entities.Metadata; [Index(nameof(Id), nameof(SeriesId), IsUnique = true)] -public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata +public class SeriesMetadata : IHasConcurrencyToken { public int Id { get; set; } public string Summary { get; set; } = string.Empty; + public ICollection CollectionTags { get; set; } = new List(); + + public ICollection Genres { get; set; } = new List(); + public ICollection Tags { get; set; } = new List(); + /// + /// All people attached at a Series level. + /// + public ICollection People { get; set; } = new List(); + /// /// Highest Age Rating from all Chapters /// @@ -43,13 +49,8 @@ public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata /// /// This is not populated from Chapters of the Series public string WebLinks { get; set; } = string.Empty; - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } = []; - - #region Locks + // Locks public bool LanguageLocked { get; set; } public bool SummaryLocked { get; set; } /// @@ -67,36 +68,17 @@ public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata public bool ColoristLocked { get; set; } public bool EditorLocked { get; set; } public bool InkerLocked { get; set; } - public bool ImprintLocked { get; set; } public bool LettererLocked { get; set; } public bool PencillerLocked { get; set; } public bool PublisherLocked { get; set; } public bool TranslatorLocked { get; set; } - public bool TeamLocked { get; set; } - public bool LocationLocked { get; set; } public bool CoverArtistLocked { get; set; } public bool ReleaseYearLocked { get; set; } - #endregion - #region Relationships - - [Obsolete("Use AppUserCollection instead")] - public ICollection CollectionTags { get; set; } = new List(); - - public ICollection Genres { get; set; } = new List(); - public ICollection Tags { get; set; } = new List(); - - /// - /// All people attached at a Series level. - /// - public ICollection People { get; set; } = new List(); - - public int SeriesId { get; set; } + // Relationship public Series Series { get; set; } = null!; - - #endregion - + public int SeriesId { get; set; } /// [ConcurrencyCheck] @@ -108,26 +90,4 @@ public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata { RowVersion++; } - - /// - /// Any People in this Role present - /// - /// - /// - public bool AnyOfRole(PersonRole role) - { - return People.Any(p => p.Role == role); - } - - /// - /// Are all instances of the role from Kavita+ - /// - /// - /// - public bool AllKavitaPlus(PersonRole role) - { - var people = People.Where(p => p.Role == role); - if (people.Any()) return people.All(p => p.KavitaPlusConnection); - return false; - } } diff --git a/API/Entities/MetadataMatching/MetadataFieldMapping.cs b/API/Entities/MetadataMatching/MetadataFieldMapping.cs deleted file mode 100644 index e7dd88c03..000000000 --- a/API/Entities/MetadataMatching/MetadataFieldMapping.cs +++ /dev/null @@ -1,26 +0,0 @@ -using API.Entities.Enums; -using API.Entities.MetadataMatching; - -namespace API.Entities; - -public class MetadataFieldMapping -{ - public int Id { get; set; } - public MetadataFieldType SourceType { get; set; } - public MetadataFieldType DestinationType { get; set; } - /// - /// The string in the source - /// - public string SourceValue { get; set; } - /// - /// Write the string as this in the Destination (can also just be the Source) - /// - public string DestinationValue { get; set; } - /// - /// If true, the tag will be Moved over vs Copied over - /// - public bool ExcludeFromSource { get; set; } - - public int MetadataSettingsId { get; set; } - public virtual MetadataSettings MetadataSettings { get; set; } -} diff --git a/API/Entities/MetadataMatching/MetadataSettingField.cs b/API/Entities/MetadataMatching/MetadataSettingField.cs deleted file mode 100644 index 9333c269e..000000000 --- a/API/Entities/MetadataMatching/MetadataSettingField.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace API.Entities.MetadataMatching; - -/// -/// Represents which field that can be written to as an override when already locked -/// -public enum MetadataSettingField -{ - #region Series Metadata - Summary = 1, - PublicationStatus = 2, - StartDate = 3, - Genres = 4, - Tags = 5, - LocalizedName = 6, - Covers = 7, - AgeRating = 8, - People = 9, - #endregion - - #region Chapter Metadata - - ChapterTitle = 10, - ChapterSummary = 11, - ChapterReleaseDate = 12, - ChapterPublisher = 13, - ChapterCovers = 14, - - #endregion - - -} diff --git a/API/Entities/MetadataMatching/MetadataSettings.cs b/API/Entities/MetadataMatching/MetadataSettings.cs deleted file mode 100644 index aeb44b619..000000000 --- a/API/Entities/MetadataMatching/MetadataSettings.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Enums; - -namespace API.Entities.MetadataMatching; - -/// -/// Handles the metadata settings for Kavita+ -/// -public class MetadataSettings -{ - public int Id { get; set; } - /// - /// If writing any sort of metadata from upstream (AniList, Hardcover) source is allowed - /// - public bool Enabled { get; set; } - - #region Series Metadata - - /// - /// Allow the Summary to be written - /// - public bool EnableSummary { get; set; } - /// - /// Allow Publication status to be derived and updated - /// - public bool EnablePublicationStatus { get; set; } - /// - /// Allow Relationships between series to be set - /// - public bool EnableRelationships { get; set; } - /// - /// Allow People to be created (including downloading images) - /// - public bool EnablePeople { get; set; } - /// - /// Allow Start date to be set within the Series - /// - public bool EnableStartDate { get; set; } - /// - /// Allow setting the Localized name - /// - public bool EnableLocalizedName { get; set; } - /// - /// Allow setting the cover image - /// - public bool EnableCoverImage { get; set; } - #endregion - - #region Chapter Metadata - /// - /// Allow Summary to be set within Chapter/Issue - /// - public bool EnableChapterSummary { get; set; } - /// - /// Allow Release Date to be set within Chapter/Issue - /// - public bool EnableChapterReleaseDate { get; set; } - /// - /// Allow Title to be set within Chapter/Issue - /// - public bool EnableChapterTitle { get; set; } - /// - /// Allow Publisher to be set within Chapter/Issue - /// - public bool EnableChapterPublisher { get; set; } - /// - /// Allow setting the cover image for the Chapter/Issue - /// - public bool EnableChapterCoverImage { get; set; } - #endregion - - // Need to handle the Genre/tags stuff - public bool EnableGenres { get; set; } = true; - public bool EnableTags { get; set; } = true; - - /// - /// For Authors and Writers, how should names be stored (Exclusively applied for AniList). This does not affect Character names. - /// - public bool FirstLastPeopleNaming { get; set; } - - /// - /// Any Genres or Tags that if present, will trigger an Age Rating Override. Highest rating will be prioritized for matching. - /// - public Dictionary AgeRatingMappings { get; set; } - - /// - /// A list of rules that allow mapping a genre/tag to another genre/tag - /// - public List FieldMappings { get; set; } - - /// - /// A list of overrides that will enable writing to locked fields - /// - public List Overrides { get; set; } - - /// - /// Do not allow any Genre/Tag in this list to be written to Kavita - /// - public List Blacklist { get; set; } - - /// - /// Only allow these Tags to be written to Kavita - /// - public List Whitelist { get; set; } - - /// - /// Which Roles to allow metadata downloading for - /// - public List PersonRoles { get; set; } -} diff --git a/API/Entities/Person.cs b/API/Entities/Person.cs new file mode 100644 index 000000000..eeb21d6b1 --- /dev/null +++ b/API/Entities/Person.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using API.Entities.Enums; +using API.Entities.Metadata; + +namespace API.Entities; + +public class Person +{ + public int Id { get; set; } + public required string Name { get; set; } + public required string NormalizedName { get; set; } + public required PersonRole Role { get; set; } + + // Relationships + public ICollection SeriesMetadatas { get; set; } = null!; + public ICollection ChapterMetadatas { get; set; } = null!; +} diff --git a/API/Entities/Person/ChapterPeople.cs b/API/Entities/Person/ChapterPeople.cs deleted file mode 100644 index c6a08a7dd..000000000 --- a/API/Entities/Person/ChapterPeople.cs +++ /dev/null @@ -1,23 +0,0 @@ -using API.Entities.Enums; - -namespace API.Entities.Person; - -public class ChapterPeople -{ - public int ChapterId { get; set; } - public virtual Chapter Chapter { get; set; } - - public int PersonId { get; set; } - public virtual Person Person { get; set; } - - /// - /// The source of this connection. If not Kavita, this implies Metadata Download linked this and it can be removed between matches - /// - public bool KavitaPlusConnection { get; set; } - /// - /// A weight that allows lower numbers to sort first - /// - public int OrderWeight { get; set; } - - public required PersonRole Role { get; set; } -} diff --git a/API/Entities/Person/Person.cs b/API/Entities/Person/Person.cs deleted file mode 100644 index ed57fd6d3..000000000 --- a/API/Entities/Person/Person.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Interfaces; - -namespace API.Entities.Person; - -public class Person : IHasCoverImage -{ - public int Id { get; set; } - public required string Name { get; set; } - public required string NormalizedName { get; set; } - public ICollection Aliases { get; set; } = []; - - public string? CoverImage { get; set; } - public bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } - - public string Description { get; set; } - /// - /// ASIN for person - /// - /// Can be used for Amazon author lookup - public string? Asin { get; set; } - - /// - /// https://anilist.co/staff/{AniListId}/ - /// - /// Kavita+ Only - public int AniListId { get; set; } = 0; - /// - /// https://myanimelist.net/people/{MalId}/ - /// https://myanimelist.net/character/{MalId}/CharacterName - /// - /// Kavita+ Only - public long MalId { get; set; } = 0; - /// - /// https://hardcover.app/authors/{HardcoverId} - /// - /// Kavita+ Only - public string? HardcoverId { get; set; } - - /// - /// https://metron.cloud/creator/{slug}/ - /// - /// Kavita+ Only - //public long MetronId { get; set; } = 0; - - // Relationships - public ICollection ChapterPeople { get; set; } = []; - public ICollection SeriesMetadataPeople { get; set; } = []; - - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } -} diff --git a/API/Entities/Person/PersonAlias.cs b/API/Entities/Person/PersonAlias.cs deleted file mode 100644 index f053f608d..000000000 --- a/API/Entities/Person/PersonAlias.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace API.Entities.Person; - -public class PersonAlias -{ - public int Id { get; set; } - public required string Alias { get; set; } - public required string NormalizedAlias { get; set; } - - public int PersonId { get; set; } - public Person Person { get; set; } -} diff --git a/API/Entities/Person/SeriesMetadataPeople.cs b/API/Entities/Person/SeriesMetadataPeople.cs deleted file mode 100644 index caea10cd6..000000000 --- a/API/Entities/Person/SeriesMetadataPeople.cs +++ /dev/null @@ -1,24 +0,0 @@ -using API.Entities.Enums; -using API.Entities.Metadata; - -namespace API.Entities.Person; - -public class SeriesMetadataPeople -{ - public int SeriesMetadataId { get; set; } - public virtual SeriesMetadata SeriesMetadata { get; set; } - - public int PersonId { get; set; } - public virtual Person Person { get; set; } - - /// - /// The source of this connection. If not Kavita, this implies Metadata Download linked this and it can be removed between matches - /// - public bool KavitaPlusConnection { get; set; } = false; - /// - /// A weight that allows lower numbers to sort first - /// - public int OrderWeight { get; set; } - - public required PersonRole Role { get; set; } -} diff --git a/API/Entities/ReadingList.cs b/API/Entities/ReadingList.cs index 4a11845af..857d5bd42 100644 --- a/API/Entities/ReadingList.cs +++ b/API/Entities/ReadingList.cs @@ -10,7 +10,7 @@ namespace API.Entities; /// /// This is a collection of which represent individual chapters and an order. /// -public class ReadingList : IEntityDate, IHasCoverImage +public class ReadingList : IEntityDate { public int Id { get; init; } public required string Title { get; set; } @@ -23,9 +23,11 @@ public class ReadingList : IEntityDate, IHasCoverImage /// Reading lists that are promoted are only done by admins /// public bool Promoted { get; set; } + /// + /// Absolute path to the (managed) image file + /// + /// The file is managed internally to Kavita's APPDIR public string? CoverImage { get; set; } - public string? PrimaryColor { get; set; } - public string? SecondaryColor { get; set; } public bool CoverImageLocked { get; set; } /// @@ -59,10 +61,4 @@ public class ReadingList : IEntityDate, IHasCoverImage // Relationships public int AppUserId { get; set; } public AppUser AppUser { get; set; } = null!; - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } } diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index 8adfdcc2e..2fd36eef3 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -28,7 +28,7 @@ public class ScrobbleEvent : IEntityDate /// public string? ReviewBody { get; set; } public string? ReviewTitle { get; set; } - public required PlusMediaFormat Format { get; set; } + public required MediaFormat Format { get; set; } /// /// Depends on the ScrobbleEvent if filled in /// @@ -36,20 +36,12 @@ public class ScrobbleEvent : IEntityDate /// /// Depends on the ScrobbleEvent if filled in /// - public float? VolumeNumber { get; set; } + public int? VolumeNumber { get; set; } /// /// Has this event been processed and pushed to Provider /// public bool IsProcessed { get; set; } /// - /// Was there an error processing this event - /// - public bool IsErrored { get; set; } - /// - /// The error details - /// - public string? ErrorDetails { get; set; } - /// /// The date this was processed /// public DateTime? ProcessDateUtc { get; set; } @@ -68,14 +60,4 @@ public class ScrobbleEvent : IEntityDate public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } - - /// - /// Sets the ErrorDetail and marks the event as - /// - /// - public void SetErrorMessage(string errorMessage) - { - ErrorDetails = errorMessage; - IsErrored = true; - } } diff --git a/API/Entities/Scrobble/ScrobbleEventFilter.cs b/API/Entities/Scrobble/ScrobbleEventFilter.cs index 1153e90e9..526843b68 100644 --- a/API/Entities/Scrobble/ScrobbleEventFilter.cs +++ b/API/Entities/Scrobble/ScrobbleEventFilter.cs @@ -15,8 +15,4 @@ public class ScrobbleEventFilter /// A query to search against ///
public string Query { get; set; } - /// - /// Include reviews in the result - Note: Review Scrobbling is disabled - /// - public bool IncludeReviews { get; set; } = false; } diff --git a/API/Entities/Scrobble/ScrobbleEventSortField.cs b/API/Entities/Scrobble/ScrobbleEventSortField.cs index 51b3a2146..729ac7fbe 100644 --- a/API/Entities/Scrobble/ScrobbleEventSortField.cs +++ b/API/Entities/Scrobble/ScrobbleEventSortField.cs @@ -7,6 +7,5 @@ public enum ScrobbleEventSortField LastModified = 2, Type= 3, Series = 4, - IsProcessed = 5, - ScrobbleEventFilter = 6 + IsProcessed = 5 } diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 4f06ab0fc..e4403eb84 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -6,7 +6,7 @@ using API.Entities.Metadata; namespace API.Entities; -public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage +public class Series : IEntityDate, IHasReadTimeEstimate { public int Id { get; set; } /// @@ -38,7 +38,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage /// public DateTime Created { get; set; } /// - /// Whenever a modification occurs. ex: New volumes, removed volumes, title update, etc + /// Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc /// public DateTime LastModified { get; set; } @@ -64,11 +64,6 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage /// must be used before setting public string? FolderPath { get; set; } /// - /// Lowest path (that is under library root) that contains all files for the series. - /// - /// must be used before setting - public string? LowestFolderPath { get; set; } - /// /// Last time the folder was scanned /// public DateTime LastFolderScanned { get; set; } @@ -81,9 +76,6 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage ///
public MangaFormat Format { get; set; } = MangaFormat.Unknown; - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; - public bool SortNameLocked { get; set; } public bool LocalizedNameLocked { get; set; } @@ -101,25 +93,12 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public int MinHoursToRead { get; set; } public int MaxHoursToRead { get; set; } - public float AvgHoursToRead { get; set; } - - #region KavitaPlus - /// - /// Do not match the series with any external Metadata service. This will automatically opt it out of scrobbling. - /// - public bool DontMatch { get; set; } - /// - /// If the series was unable to match, it will be blacklisted until a manual metadata match overrides it - /// - public bool IsBlacklisted { get; set; } - #endregion + public int AvgHoursToRead { get; set; } public SeriesMetadata Metadata { get; set; } = null!; - public ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!; public ICollection Ratings { get; set; } = null!; public ICollection Progress { get; set; } = null!; - public ICollection Collections { get; set; } = null!; /// /// Relations to other Series, like Sequels, Prequels, etc @@ -129,14 +108,11 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public ICollection RelationOf { get; set; } = null!; - - // Relationships public List Volumes { get; set; } = null!; public Library Library { get; set; } = null!; public int LibraryId { get; set; } - public void UpdateLastFolderScanned() { LastFolderScanned = DateTime.Now; @@ -148,28 +124,4 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage LastChapterAdded = DateTime.Now; LastChapterAddedUtc = DateTime.UtcNow; } - - public bool MatchesSeriesByName(string nameNormalized, string localizedNameNormalized) - { - return NormalizedName == nameNormalized || - NormalizedLocalizedName == nameNormalized || - NormalizedName == localizedNameNormalized || - NormalizedLocalizedName == localizedNameNormalized; - } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } - - /// - /// Is this Series capable of Scrobbling - /// - /// This includes if there is no Match/Manual Match needed, the series is blacklisted, or has a NoMatch - /// - public bool WillScrobble() - { - return !IsBlacklisted && !DontMatch; - } } diff --git a/API/Entities/SideNavStreamType.cs b/API/Entities/SideNavStreamType.cs index 62f429889..3150bf08e 100644 --- a/API/Entities/SideNavStreamType.cs +++ b/API/Entities/SideNavStreamType.cs @@ -10,5 +10,4 @@ public enum SideNavStreamType ExternalSource = 6, AllSeries = 7, WantToRead = 8, - BrowsePeople = 9 } diff --git a/API/Entities/SiteTheme.cs b/API/Entities/SiteTheme.cs index 107dca556..09b348cb8 100644 --- a/API/Entities/SiteTheme.cs +++ b/API/Entities/SiteTheme.cs @@ -37,30 +37,4 @@ public class SiteTheme : IEntityDate, ITheme public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } - - #region ThemeBrowser - - /// - /// The Url on the repo to download the file from - /// - public string? GitHubPath { get; set; } - /// - /// Hash of the Css File - /// - public string? ShaHash { get; set; } - /// - /// Pipe (|) separated urls of the images. Empty string if - /// - public string PreviewUrls { get; set; } - // /// - // /// A description about the theme - // /// - public string Description { get; set; } - // /// - // /// Author of the Theme - // /// - public string Author { get; set; } - public string CompatibleVersion { get; set; } - - #endregion } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 5338494e6..f5239e708 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -1,48 +1,32 @@ using System; using System.Collections.Generic; -using System.Globalization; using API.Entities.Interfaces; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; namespace API.Entities; -public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage +public class Volume : IEntityDate, IHasReadTimeEstimate { public int Id { get; set; } /// - /// A String representation of the volume number. Allows for floats. Can also include a range (1-2). + /// A String representation of the volume number. Allows for floats. /// /// For Books with Series_index, this will map to the Series Index. public required string Name { get; set; } /// - /// This is just the original Parsed volume number for lookups - /// - public string LookupName { get; set; } - /// /// The minimum number in the Name field in Int form /// - /// Removed in v0.7.13.8, this was an int and we need the ability to have 0.5 volumes render on the UI - [Obsolete("Use MinNumber and MaxNumber instead")] - public int Number { get; set; } - /// - /// The minimum number in the Name field - /// - public required float MinNumber { get; set; } - /// - /// The maximum number in the Name field (same as Minimum if Name isn't a range) - /// - public required float MaxNumber { get; set; } + public required int Number { get; set; } + public IList Chapters { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } + /// + /// Absolute path to the (managed) image file + /// + /// The file is managed internally to Kavita's APPDIR public string? CoverImage { get; set; } - public bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } - /// /// Total pages of all chapters in this volume /// @@ -54,32 +38,11 @@ public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public long WordCount { get; set; } public int MinHoursToRead { get; set; } public int MaxHoursToRead { get; set; } - public float AvgHoursToRead { get; set; } + public int AvgHoursToRead { get; set; } // Relationships - public IList Chapters { get; set; } = null!; public Series Series { get; set; } = null!; public int SeriesId { get; set; } - /// - /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. - /// - /// - public string GetNumberTitle() - { - if (MinNumber.Equals(MaxNumber)) - { - return MinNumber.ToString(CultureInfo.InvariantCulture); - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}-{MaxNumber.ToString(CultureInfo.InvariantCulture)}"; - } - - public void ResetColorScape() - { - PrimaryColor = string.Empty; - SecondaryColor = string.Empty; - } - } diff --git a/API/Errors/ApiException.cs b/API/Errors/ApiException.cs index 60d93729c..d9c1a755a 100644 --- a/API/Errors/ApiException.cs +++ b/API/Errors/ApiException.cs @@ -2,3 +2,4 @@ #nullable enable public record ApiException(int Status, string? Message = null, string? Details = null); +#nullable disable diff --git a/API/Extensions/AppUserExtensions.cs b/API/Extensions/AppUserExtensions.cs index be3d2c064..2fbf865d9 100644 --- a/API/Extensions/AppUserExtensions.cs +++ b/API/Extensions/AppUserExtensions.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; using System.Linq; -using API.Data.Misc; using API.Entities; using API.Helpers; namespace API.Extensions; -#nullable enable public static class AppUserExtensions { @@ -45,13 +43,4 @@ public static class AppUserExtensions OrderableHelper.ReorderItems(user.SideNavStreams); } - - public static AgeRestriction GetAgeRestriction(this AppUser user) - { - return new AgeRestriction() - { - AgeRating = user.AgeRestriction, - IncludeUnknowns = user.AgeRestrictionIncludeUnknowns, - }; - } } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index bd4783f25..2ceeda942 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -12,13 +12,11 @@ using API.SignalR.Presence; using Kavita.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace API.Extensions; - public static class ApplicationServiceExtensions { public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) @@ -46,19 +44,16 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -70,18 +65,15 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddSqLite(); services.AddSignalR(opt => opt.EnableDetailedErrors = true); @@ -89,21 +81,21 @@ public static class ApplicationServiceExtensions services.AddEasyCaching(options => { options.UseInMemory(EasyCacheProfiles.Favicon); - options.UseInMemory(EasyCacheProfiles.Publisher); + options.UseInMemory(EasyCacheProfiles.License); options.UseInMemory(EasyCacheProfiles.Library); options.UseInMemory(EasyCacheProfiles.RevokedJwt); - options.UseInMemory(EasyCacheProfiles.LocaleOptions); + options.UseInMemory(EasyCacheProfiles.Filter); // KavitaPlus stuff + options.UseInMemory(EasyCacheProfiles.KavitaPlusReviews); + options.UseInMemory(EasyCacheProfiles.KavitaPlusRecommendations); + options.UseInMemory(EasyCacheProfiles.KavitaPlusRatings); options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); - options.UseInMemory(EasyCacheProfiles.License); - options.UseInMemory(EasyCacheProfiles.LicenseInfo); - options.UseInMemory(EasyCacheProfiles.KavitaPlusMatchSeries); }); services.AddMemoryCache(options => { - options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB + options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 50 MB options.CompactionPercentage = 0.1; // LRU compaction (10%) }); @@ -117,14 +109,10 @@ public static class ApplicationServiceExtensions { services.AddDbContextPool(options => { - options.UseSqlite("Data source=config/kavita.db", builder => - { - builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }); + options.UseSqlite("Data source=config/kavita.db"); options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); - options.ConfigureWarnings(warnings => - warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); }); } } diff --git a/API/Extensions/ChapterListExtensions.cs b/API/Extensions/ChapterListExtensions.cs index 5456a6e16..9f14c22ee 100644 --- a/API/Extensions/ChapterListExtensions.cs +++ b/API/Extensions/ChapterListExtensions.cs @@ -1,13 +1,10 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using API.Entities; using API.Helpers; -using API.Helpers.Builders; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; -#nullable enable public static class ChapterListExtensions { @@ -25,20 +22,15 @@ public static class ChapterListExtensions /// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info /// is then, the filename is used to search against Range or if filename exists within Files of said Chapter. /// - /// This uses GetNumberTitle() to calculate the Range to compare against the info.Chapters /// /// /// public static Chapter? GetChapterByRange(this IEnumerable chapters, ParserInfo info) { - var normalizedPath = Parser.NormalizePath(info.FullFilePath); var specialTreatment = info.IsSpecialInfo(); - // NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter - var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build(); - fakeChapter.UpdateFrom(info); return specialTreatment - ? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) - : chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle()); + ? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath))) + : chapters.FirstOrDefault(c => c.Range == info.Chapters); } /// @@ -48,6 +40,6 @@ public static class ChapterListExtensions /// public static int MinimumReleaseYear(this IList chapters) { - return chapters.Select(v => v.ReleaseDate.Year).Where(NumberHelper.IsValidYear).DefaultIfEmpty().Min(); + return chapters.Select(v => v.ReleaseDate.Year).Where(y => NumberHelper.IsValidYear(y)).DefaultIfEmpty().Min(); } } diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/API/Extensions/ClaimsPrincipalExtensions.cs index 2e86f8bbd..07d94b23f 100644 --- a/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/API/Extensions/ClaimsPrincipalExtensions.cs @@ -3,27 +3,20 @@ using Kavita.Common; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace API.Extensions; -#nullable enable public static class ClaimsPrincipalExtensions { - private const string NotAuthenticatedMessage = "User is not authenticated"; - /// - /// Get's the authenticated user's username - /// - /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username - /// - /// - /// public static string GetUsername(this ClaimsPrincipal user) { - var userClaim = user.FindFirst(JwtRegisteredClaimNames.Name) ?? throw new KavitaException(NotAuthenticatedMessage); + var userClaim = user.FindFirst(JwtRegisteredClaimNames.Name); + if (userClaim == null) throw new KavitaException("User is not authenticated"); return userClaim.Value; } public static int GetUserId(this ClaimsPrincipal user) { - var userClaim = user.FindFirst(ClaimTypes.NameIdentifier) ?? throw new KavitaException(NotAuthenticatedMessage); + var userClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userClaim == null) throw new KavitaException("User is not authenticated"); return int.Parse(userClaim.Value); } } diff --git a/API/Extensions/DateTimeExtensions.cs b/API/Extensions/DateTimeExtensions.cs index a5006261f..3967641ef 100644 --- a/API/Extensions/DateTimeExtensions.cs +++ b/API/Extensions/DateTimeExtensions.cs @@ -1,7 +1,6 @@ using System; namespace API.Extensions; -#nullable enable public static class DateTimeExtensions { diff --git a/API/Extensions/DoubleExtensions.cs b/API/Extensions/DoubleExtensions.cs deleted file mode 100644 index 3deb37ffb..000000000 --- a/API/Extensions/DoubleExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace API.Extensions; - -public static class DoubleExtensions -{ - private const float Tolerance = 0.001f; - - /// - /// Used to compare 2 floats together - /// - /// - /// - /// - public static bool Is(this double a, double? b) - { - if (!b.HasValue) return false; - return Math.Abs((float) (a - b)) < Tolerance; - } - - public static bool IsNot(this double a, double? b) - { - if (!b.HasValue) return false; - return Math.Abs((float) (a - b)) > Tolerance; - } -} diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index 924ae8b89..bede8e721 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -2,7 +2,6 @@ using API.Entities.Enums; namespace API.Extensions; -#nullable enable public static class EncodeFormatExtensions { diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 9bc06bab4..8dc2377df 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -3,12 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using API.Data.Misc; -using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; namespace API.Extensions; -#nullable enable public static class EnumerableExtensions { @@ -44,28 +41,4 @@ public static class EnumerableExtensions return q; } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } } diff --git a/API/Extensions/FileInfoExtensions.cs b/API/Extensions/FileInfoExtensions.cs index 1403486dd..1f4ea62e1 100644 --- a/API/Extensions/FileInfoExtensions.cs +++ b/API/Extensions/FileInfoExtensions.cs @@ -2,7 +2,6 @@ using System.IO; namespace API.Extensions; -#nullable enable public static class FileInfoExtensions { diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs deleted file mode 100644 index 24073f642..000000000 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Extensions; - -public static class FileTypeGroupExtensions -{ - public static string GetRegex(this FileTypeGroup fileTypeGroup) - { - switch (fileTypeGroup) - { - case FileTypeGroup.Archive: - return Parser.ArchiveFileExtensions; - case FileTypeGroup.Epub: - return Parser.EpubFileExtension; - case FileTypeGroup.Pdf: - return Parser.PdfFileExtension; - case FileTypeGroup.Images: - return Parser.ImageFileExtensions; - default: - throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); - } - } -} diff --git a/API/Extensions/FilterDtoExtensions.cs b/API/Extensions/FilterDtoExtensions.cs index 7a55f7db9..bc5b4eb52 100644 --- a/API/Extensions/FilterDtoExtensions.cs +++ b/API/Extensions/FilterDtoExtensions.cs @@ -4,7 +4,6 @@ using API.DTOs.Filtering; using API.Entities.Enums; namespace API.Extensions; -#nullable enable public static class FilterDtoExtensions { diff --git a/API/Extensions/FloatExtensions.cs b/API/Extensions/FloatExtensions.cs deleted file mode 100644 index 6fa553239..000000000 --- a/API/Extensions/FloatExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace API.Extensions; - -public static class FloatExtensions -{ - private const float Tolerance = 0.001f; - - /// - /// Used to compare 2 floats together - /// - /// - /// - /// - public static bool Is(this float a, float? b) - { - if (!b.HasValue) return false; - return Math.Abs((float) (a - b)) < Tolerance; - } - - public static bool IsNot(this float a, float? b) - { - if (!b.HasValue) return false; - return Math.Abs((float) (a - b)) > Tolerance; - } -} diff --git a/API/Extensions/FlurlExtensions.cs b/API/Extensions/FlurlExtensions.cs deleted file mode 100644 index 62d8543b6..000000000 --- a/API/Extensions/FlurlExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Flurl.Http; -using Kavita.Common; -using Kavita.Common.EnvironmentInfo; - -namespace API.Extensions; -#nullable enable - -public static class FlurlExtensions -{ - public static IFlurlRequest WithKavitaPlusHeaders(this string request, string license, string? anilistToken = null) - { - return request - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-license-key", license) - .WithHeader("x-installId", HashUtil.ServerToken()) - .WithHeader("x-anilist-token", anilistToken ?? string.Empty) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)); - } - - public static IFlurlRequest WithBasicHeaders(this string request, string apiKey) - { - return request - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", apiKey) - .WithHeader("x-installId", HashUtil.ServerToken()) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)); - } -} diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index fbf828104..4a75dfece 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace API.Extensions; -#nullable enable public static class HttpExtensions { @@ -21,8 +20,8 @@ public static class HttpExtensions PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - response.Headers.Append("Pagination", JsonSerializer.Serialize(paginationHeader, options)); - response.Headers.Append("Access-Control-Expose-Headers", "Pagination"); + response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options)); + response.Headers.Add("Access-Control-Expose-Headers", "Pagination"); } /// @@ -33,7 +32,7 @@ public static class HttpExtensions public static void AddCacheHeader(this HttpResponse response, byte[] content) { if (content is not {Length: > 0}) return; - response.Headers.Append(HeaderNames.ETag, string.Concat(SHA256.HashData(content).Select(x => x.ToString("X2")))); + response.Headers.Add(HeaderNames.ETag, string.Concat(SHA256.HashData(content).Select(x => x.ToString("X2")))); response.Headers.CacheControl = $"private,max-age=100"; } @@ -47,7 +46,7 @@ public static class HttpExtensions { if (filename is not {Length: > 0}) return; var hashContent = filename + File.GetLastWriteTimeUtc(filename); - response.Headers.Append("ETag", string.Concat(SHA256.HashData(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")))); + response.Headers.Add("ETag", string.Concat(SHA256.HashData(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")))); if (maxAge != 10) { response.Headers.CacheControl = $"max-age={maxAge}"; diff --git a/API/Extensions/IHasKPlusMetadataExtensions.cs b/API/Extensions/IHasKPlusMetadataExtensions.cs deleted file mode 100644 index 84e35adc4..000000000 --- a/API/Extensions/IHasKPlusMetadataExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; - -namespace API.Extensions; - -public static class IHasKPlusMetadataExtensions -{ - - public static bool HasSetKPlusMetadata(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field) - { - return hasKPlusMetadata.KPlusOverrides.Contains(field); - } - - public static void AddKPlusOverride(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field) - { - if (hasKPlusMetadata.KPlusOverrides.Contains(field)) return; - - hasKPlusMetadata.KPlusOverrides.Add(field); - } - -} diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 9549e9a2c..5dc547362 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace API.Extensions; -#nullable enable public static class IdentityServiceExtensions { diff --git a/API/Extensions/ImageExtensions.cs b/API/Extensions/ImageExtensions.cs deleted file mode 100644 index 5779b18ec..000000000 --- a/API/Extensions/ImageExtensions.cs +++ /dev/null @@ -1,437 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Image = SixLabors.ImageSharp.Image; - -namespace API.Extensions; - -public static class ImageExtensions -{ - - /// - /// Structure to hold various image quality metrics - /// - private sealed class ImageQualityMetrics - { - public int Width { get; set; } - public int Height { get; set; } - public bool IsColor { get; set; } - public double Colorfulness { get; set; } - public double Contrast { get; set; } - public double Sharpness { get; set; } - public double NoiseLevel { get; set; } - } - - - /// - /// Calculate a similarity score (0-1f) based on resolution difference and MSE. - /// - /// Path to first image - /// Path to the second image - /// Similarity score between 0-1, where 1 is identical - public static float CalculateSimilarity(this string imagePath1, string imagePath2) - { - if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) - { - throw new FileNotFoundException("One or both image files do not exist"); - } - - // Load both images as Rgba32 (consistent with the rest of the code) - using var img1 = Image.Load(imagePath1); - using var img2 = Image.Load(imagePath2); - - // Calculate resolution difference factor - var res1 = img1.Width * img1.Height; - var res2 = img2.Width * img2.Height; - var resolutionDiff = Math.Abs(res1 - res2) / (float) Math.Max(res1, res2); - - // Calculate mean squared error for pixel differences - var mse = img1.GetMeanSquaredError(img2); - - // Normalize MSE (65025 = 255², which is the max possible squared difference per channel) - var normalizedMse = 1f - Math.Min(1f, mse / 65025f); - - // Final similarity score (weighted average of resolution difference and color difference) - return Math.Max(0f, 1f - (resolutionDiff * 0.5f) - (1f - normalizedMse) * 0.5f); - } - - /// - /// Smaller is better - /// - /// - /// - /// - public static float GetMeanSquaredError(this Image img1, Image img2) - { - if (img1.Width != img2.Width || img1.Height != img2.Height) - { - img2.Mutate(x => x.Resize(img1.Width, img1.Height)); - } - - double totalDiff = 0; - for (var y = 0; y < img1.Height; y++) - { - for (var x = 0; x < img1.Width; x++) - { - var pixel1 = img1[x, y]; - var pixel2 = img2[x, y]; - - var diff = Math.Pow(pixel1.R - pixel2.R, 2) + - Math.Pow(pixel1.G - pixel2.G, 2) + - Math.Pow(pixel1.B - pixel2.B, 2); - totalDiff += diff; - } - } - - return (float) (totalDiff / (img1.Width * img1.Height)); - } - - /// - /// Determines which image is "better" based on multiple quality factors - /// using only the cross-platform ImageSharp library - /// - /// Path to first image - /// Path to the second image - /// Whether to prefer color images over grayscale (default: true) - /// The path of the better image - public static string GetBetterImage(this string imagePath1, string imagePath2, bool preferColor = true) - { - if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) - { - throw new FileNotFoundException("One or both image files do not exist"); - } - - // Quick metadata check to get width/height without loading full pixel data - var info1 = Image.Identify(imagePath1); - var info2 = Image.Identify(imagePath2); - - // Calculate resolution factor - double resolutionFactor1 = info1.Width * info1.Height; - double resolutionFactor2 = info2.Width * info2.Height; - - // If one image is significantly higher resolution (3x or more), just pick it - // This avoids fully loading both images when the choice is obvious - if (resolutionFactor1 > resolutionFactor2 * 3) - return imagePath1; - if (resolutionFactor2 > resolutionFactor1 * 3) - return imagePath2; - - // Otherwise, we need to analyze the actual image data for both - - // NOTE: We HAVE to use these scope blocks and load image here otherwise memory-mapped section exception will occur - ImageQualityMetrics metrics1; - using (var img1 = Image.Load(imagePath1)) - { - metrics1 = GetImageQualityMetrics(img1); - } - - ImageQualityMetrics metrics2; - using (var img2 = Image.Load(imagePath2)) - { - metrics2 = GetImageQualityMetrics(img2); - } - - - // If one is color, and one is grayscale, then we prefer color - if (preferColor && metrics1.IsColor != metrics2.IsColor) - { - return metrics1.IsColor ? imagePath1 : imagePath2; - } - - // Calculate overall quality scores - var score1 = CalculateOverallScore(metrics1); - var score2 = CalculateOverallScore(metrics2); - - return score1 >= score2 ? imagePath1 : imagePath2; - } - - - /// - /// Calculate a weighted overall score based on metrics - /// - private static double CalculateOverallScore(ImageQualityMetrics metrics) - { - // Resolution factor (normalized to HD resolution) - var resolutionFactor = Math.Min(1.0, (metrics.Width * metrics.Height) / (double) (1920 * 1080)); - - // Color factor - var colorFactor = metrics.IsColor ? (0.5 + 0.5 * metrics.Colorfulness) : 0.3; - - // Quality factors - var contrastFactor = Math.Min(1.0, metrics.Contrast); - var sharpnessFactor = Math.Min(1.0, metrics.Sharpness); - - // Noise penalty (less noise is better) - var noisePenalty = Math.Max(0, 1.0 - metrics.NoiseLevel); - - // Weighted combination - return (resolutionFactor * 0.35) + - (colorFactor * 0.3) + - (contrastFactor * 0.15) + - (sharpnessFactor * 0.15) + - (noisePenalty * 0.05); - } - - /// - /// Gets quality metrics for an image - /// - private static ImageQualityMetrics GetImageQualityMetrics(Image image) - { - // Create a smaller version if the image is large to speed up analysis - Image workingImage; - if (image.Width > 512 || image.Height > 512) - { - workingImage = image.Clone(ctx => ctx.Resize( - new ResizeOptions { - Size = new Size(512), - Mode = ResizeMode.Max - })); - } - else - { - workingImage = image.Clone(); - } - - var metrics = new ImageQualityMetrics - { - Width = image.Width, - Height = image.Height - }; - - // Color analysis (is the image color or grayscale?) - var colorInfo = AnalyzeColorfulness(workingImage); - metrics.IsColor = colorInfo.IsColor; - metrics.Colorfulness = colorInfo.Colorfulness; - - // Contrast analysis - metrics.Contrast = CalculateContrast(workingImage); - - // Sharpness estimation - metrics.Sharpness = EstimateSharpness(workingImage); - - // Noise estimation - metrics.NoiseLevel = EstimateNoiseLevel(workingImage); - - // Clean up - workingImage.Dispose(); - - return metrics; - } - - /// - /// Analyzes colorfulness of an image - /// - private static (bool IsColor, double Colorfulness) AnalyzeColorfulness(Image image) - { - // For performance, sample a subset of pixels - var sampleSize = Math.Min(1000, image.Width * image.Height); - var stepSize = Math.Max(1, (image.Width * image.Height) / sampleSize); - - var colorCount = 0; - List<(int R, int G, int B)> samples = []; - - // Sample pixels - for (var i = 0; i < image.Width * image.Height; i += stepSize) - { - var x = i % image.Width; - var y = i / image.Width; - - var pixel = image[x, y]; - - // Check if RGB channels differ by a threshold - // High difference indicates color, low difference indicates grayscale - var rMinusG = Math.Abs(pixel.R - pixel.G); - var rMinusB = Math.Abs(pixel.R - pixel.B); - var gMinusB = Math.Abs(pixel.G - pixel.B); - - if (rMinusG > 15 || rMinusB > 15 || gMinusB > 15) - { - colorCount++; - } - - samples.Add((pixel.R, pixel.G, pixel.B)); - } - - // Calculate colorfulness metric based on Hasler and Süsstrunk's approach - // This measures the spread and intensity of colors - if (samples.Count <= 0) return (false, 0); - - // Calculate rg and yb opponent channels - var rg = samples.Select(p => p.R - p.G).ToList(); - var yb = samples.Select(p => 0.5 * (p.R + p.G) - p.B).ToList(); - - // Calculate standard deviation and mean of opponent channels - var rgStdDev = CalculateStdDev(rg); - var ybStdDev = CalculateStdDev(yb); - var rgMean = rg.Average(); - var ybMean = yb.Average(); - - // Combine into colorfulness metric - var stdRoot = Math.Sqrt(rgStdDev * rgStdDev + ybStdDev * ybStdDev); - var meanRoot = Math.Sqrt(rgMean * rgMean + ybMean * ybMean); - - var colorfulness = stdRoot + 0.3 * meanRoot; - - // Normalize to 0-1 range (typical colorfulness is 0-100) - colorfulness = Math.Min(1.0, colorfulness / 100.0); - - var isColor = (double)colorCount / samples.Count > 0.05; - - return (isColor, colorfulness); - - } - - /// - /// Calculate standard deviation of a list of values - /// - private static double CalculateStdDev(List values) - { - var mean = values.Average(); - var sumOfSquaresOfDifferences = values.Select(val => (val - mean) * (val - mean)).Sum(); - return Math.Sqrt(sumOfSquaresOfDifferences / values.Count); - } - - /// - /// Calculate standard deviation of a list of values - /// - private static double CalculateStdDev(List values) - { - var mean = values.Average(); - var sumOfSquaresOfDifferences = values.Select(val => (val - mean) * (val - mean)).Sum(); - return Math.Sqrt(sumOfSquaresOfDifferences / values.Count); - } - - /// - /// Calculates contrast of an image - /// - private static double CalculateContrast(Image image) - { - // For performance, sample a subset of pixels - var sampleSize = Math.Min(1000, image.Width * image.Height); - var stepSize = Math.Max(1, (image.Width * image.Height) / sampleSize); - - List luminanceValues = new(); - - // Sample pixels and calculate luminance - for (var i = 0; i < image.Width * image.Height; i += stepSize) - { - var x = i % image.Width; - var y = i / image.Width; - - var pixel = image[x, y]; - - // Calculate luminance - var luminance = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); - luminanceValues.Add(luminance); - } - - if (luminanceValues.Count < 2) - return 0; - - // Use RMS contrast (root-mean-square of pixel intensity) - var mean = luminanceValues.Average(); - var sumOfSquaresOfDifferences = luminanceValues.Sum(l => Math.Pow(l - mean, 2)); - var rmsContrast = Math.Sqrt(sumOfSquaresOfDifferences / luminanceValues.Count) / mean; - - // Normalize to 0-1 range - return Math.Min(1.0, rmsContrast); - } - - /// - /// Estimates sharpness using simple Laplacian-based method - /// - private static double EstimateSharpness(Image image) - { - // For simplicity, convert to grayscale - var grayImage = new int[image.Width, image.Height]; - - // Convert to grayscale - for (var y = 0; y < image.Height; y++) - { - for (var x = 0; x < image.Width; x++) - { - var pixel = image[x, y]; - grayImage[x, y] = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); - } - } - - // Apply Laplacian filter (3x3) - // The Laplacian measures local variations - higher values indicate edges/details - double laplacianSum = 0; - var validPixels = 0; - - // Laplacian kernel: [0, 1, 0, 1, -4, 1, 0, 1, 0] - for (var y = 1; y < image.Height - 1; y++) - { - for (var x = 1; x < image.Width - 1; x++) - { - var laplacian = - grayImage[x, y - 1] + - grayImage[x - 1, y] - 4 * grayImage[x, y] + grayImage[x + 1, y] + - grayImage[x, y + 1]; - - laplacianSum += Math.Abs(laplacian); - validPixels++; - } - } - - if (validPixels == 0) - return 0; - - // Calculate variance of Laplacian - var laplacianVariance = laplacianSum / validPixels; - - // Normalize to 0-1 range (typical values range from 0-1000) - return Math.Min(1.0, laplacianVariance / 1000.0); - } - - /// - /// Estimates noise level using simple block-based variance method - /// - private static double EstimateNoiseLevel(Image image) - { - // Block size for noise estimation - const int blockSize = 8; - List blockVariances = new(); - - // Calculate variance in small blocks throughout the image - for (var y = 0; y < image.Height - blockSize; y += blockSize) - { - for (var x = 0; x < image.Width - blockSize; x += blockSize) - { - List blockValues = new(); - - // Sample block - for (var by = 0; by < blockSize; by++) - { - for (var bx = 0; bx < blockSize; bx++) - { - var pixel = image[x + bx, y + by]; - var value = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); - blockValues.Add(value); - } - } - - // Calculate variance of this block - var blockMean = blockValues.Average(); - var blockVariance = blockValues.Sum(v => Math.Pow(v - blockMean, 2)) / blockValues.Count; - blockVariances.Add(blockVariance); - } - } - - if (blockVariances.Count == 0) - return 0; - - // Sort block variances and take lowest 10% (likely uniform areas where noise is most visible) - blockVariances.Sort(); - var smoothBlocksCount = Math.Max(1, blockVariances.Count / 10); - var averageNoiseVariance = blockVariances.Take(smoothBlocksCount).Average(); - - // Normalize to 0-1 range (typical noise variances are 0-100) - return Math.Min(1.0, averageNoiseVariance / 100.0); - } -} diff --git a/API/Extensions/ParserInfoListExtensions.cs b/API/Extensions/ParserInfoListExtensions.cs index 94eb1c769..96e39176f 100644 --- a/API/Extensions/ParserInfoListExtensions.cs +++ b/API/Extensions/ParserInfoListExtensions.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using API.Entities; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; -#nullable enable public static class ParserInfoListExtensions { @@ -28,9 +26,7 @@ public static class ParserInfoListExtensions /// public static bool HasInfo(this IList infos, Chapter chapter) { - var chapterFiles = chapter.Files.Select(x => Parser.NormalizePath(x.FilePath)).ToList(); - var infoFiles = infos.Select(x => Parser.NormalizePath(x.FullFilePath)).ToList(); - return infoFiles.Intersect(chapterFiles).Any(); + return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range) + : infos.Any(v => v.Chapters == chapter.Range); } - } diff --git a/API/Extensions/PathExtensions.cs b/API/Extensions/PathExtensions.cs index 64c0616ab..f45787d1a 100644 --- a/API/Extensions/PathExtensions.cs +++ b/API/Extensions/PathExtensions.cs @@ -1,7 +1,6 @@ using System.IO; namespace API.Extensions; -#nullable enable public static class PathExtensions { diff --git a/API/Extensions/PlusMediaFormatExtensions.cs b/API/Extensions/PlusMediaFormatExtensions.cs deleted file mode 100644 index a88b9c2f9..000000000 --- a/API/Extensions/PlusMediaFormatExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Scrobbling; -using API.Entities.Enums; - -namespace API.Extensions; - -public static class PlusMediaFormatExtensions -{ - public static PlusMediaFormat ConvertToPlusMediaFormat(this LibraryType libraryType, MangaFormat? seriesFormat = null) - { - - return libraryType switch - { - LibraryType.Manga => seriesFormat is MangaFormat.Epub ? PlusMediaFormat.LightNovel : PlusMediaFormat.Manga, - LibraryType.Comic => PlusMediaFormat.Comic, - LibraryType.LightNovel => PlusMediaFormat.LightNovel, - LibraryType.Book => PlusMediaFormat.LightNovel, - LibraryType.Image => PlusMediaFormat.Manga, - LibraryType.ComicVine => PlusMediaFormat.Comic, - _ => throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null) - }; - } - - public static IEnumerable ConvertToLibraryTypes(this PlusMediaFormat plusMediaFormat) - { - return plusMediaFormat switch - { - PlusMediaFormat.Manga => [LibraryType.Manga, LibraryType.Image], - PlusMediaFormat.Comic => [LibraryType.Comic, LibraryType.ComicVine], - PlusMediaFormat.LightNovel => [LibraryType.LightNovel, LibraryType.Book, LibraryType.Manga], - PlusMediaFormat.Book => [LibraryType.LightNovel, LibraryType.Book], - _ => throw new ArgumentOutOfRangeException(nameof(plusMediaFormat), plusMediaFormat, null) - }; - } - - public static IList GetMangaFormats(this PlusMediaFormat? mediaFormat) - { - return mediaFormat.HasValue ? mediaFormat.Value.GetMangaFormats() : [MangaFormat.Archive]; - } - - public static IList GetMangaFormats(this PlusMediaFormat mediaFormat) - { - return mediaFormat switch - { - PlusMediaFormat.Manga => [MangaFormat.Archive, MangaFormat.Image], - PlusMediaFormat.Comic => [MangaFormat.Archive], - PlusMediaFormat.LightNovel => [MangaFormat.Epub, MangaFormat.Pdf], - PlusMediaFormat.Book => [MangaFormat.Epub, MangaFormat.Pdf], - _ => [MangaFormat.Archive] - }; - } - - -} diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs index 030517dbf..ed4f300a0 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs @@ -1,15 +1,14 @@ using System.Linq; using API.DTOs.Filtering; using API.Entities; -using Microsoft.EntityFrameworkCore; +using API.Extensions.QueryExtensions; namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable public class BookmarkSeriesPair { - public AppUserBookmark Bookmark { get; init; } = null!; - public Series Series { get; init; } = null!; + public AppUserBookmark Bookmark { get; set; } + public Series Series { get; set; } } public static class BookmarkSort @@ -38,9 +37,6 @@ public static class BookmarkSort SortField.TimeToRead => query.DoOrderBy(s => s.Series.AvgHoursToRead, sortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Series.Metadata.ReleaseYear, sortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max(), sortOptions), - SortField.AverageRating => query.DoOrderBy(s => s.Series.ExternalSeriesMetadata.ExternalRatings - .Where(p => p.SeriesId == s.Series.Id).Average(p => p.AverageScore), sortOptions), - SortField.Random => query.DoOrderBy(s => EF.Functions.Random(), sortOptions), _ => query }; diff --git a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs b/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs deleted file mode 100644 index c36164d9d..000000000 --- a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Entities.Person; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; - -public static class PersonFilter -{ - public static IQueryable HasPersonName(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (string.IsNullOrEmpty(queryString) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => p.Name.Equals(queryString)), - FilterComparison.BeginsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"{queryString}%")), - FilterComparison.EndsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}")), - FilterComparison.Matches => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}%")), - FilterComparison.NotEqual => queryable.Where(p => p.Name != queryString), - FilterComparison.NotContains or FilterComparison.GreaterThan or FilterComparison.GreaterThanEqual - or FilterComparison.LessThan or FilterComparison.LessThanEqual or FilterComparison.Contains - or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast - or FilterComparison.IsNotInLast or FilterComparison.MustContains - or FilterComparison.IsEmpty => - throw new KavitaException($"{comparison} not applicable for Person.Name"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, - "Filter Comparison is not supported") - }; - } - public static IQueryable HasPersonRole(this IQueryable queryable, bool condition, - FilterComparison comparison, IList roles) - { - if (roles == null || roles.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Contains or FilterComparison.MustContains => queryable.Where(p => - p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || - p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), - FilterComparison.NotContains => queryable.Where(p => - !p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) && - !p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), - FilterComparison.Equal or FilterComparison.NotEqual or FilterComparison.BeginsWith - or FilterComparison.EndsWith or FilterComparison.Matches or FilterComparison.GreaterThan - or FilterComparison.GreaterThanEqual or FilterComparison.LessThan or FilterComparison.LessThanEqual - or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast - or FilterComparison.IsNotInLast - or FilterComparison.IsEmpty => - throw new KavitaException($"{comparison} not applicable for Person.Role"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, - "Filter Comparison is not supported") - }; - } - - public static IQueryable HasPersonSeriesCount(this IQueryable queryable, bool condition, - FilterComparison comparison, int count) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() == count), - FilterComparison.GreaterThan => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() > count), - FilterComparison.GreaterThanEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() >= count), - FilterComparison.LessThan => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() < count), - FilterComparison.LessThanEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() <= count), - FilterComparison.NotEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() != count), - FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches - or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore - or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast - or FilterComparison.MustContains - or FilterComparison.IsEmpty => throw new KavitaException( - $"{comparison} not applicable for Person.SeriesCount"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") - }; - } - - public static IQueryable HasPersonChapterCount(this IQueryable queryable, bool condition, - FilterComparison comparison, int count) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() == count), - FilterComparison.GreaterThan => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() > count), - FilterComparison.GreaterThanEqual => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() >= count), - FilterComparison.LessThan => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() < count), - FilterComparison.LessThanEqual => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() <= count), - FilterComparison.NotEqual => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() != count), - FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches - or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore - or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast - or FilterComparison.MustContains - or FilterComparison.IsEmpty => throw new KavitaException( - $"{comparison} not applicable for Person.ChapterCount"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") - }; - } -} diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs deleted file mode 100644 index d7acf9381..000000000 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Data.Misc; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Metadata; -using API.Entities.Person; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; - -public static class SearchQueryableExtensions -{ - public static IQueryable Search(this IQueryable queryable, - string searchQuery, int userId, AgeRestriction userRating) - { - return queryable - .Where(uc => uc.Promoted || uc.AppUserId == userId) - .Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%") - || EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%")) - .RestrictAgainstAgeRestriction(userRating) - .OrderBy(s => s.NormalizedTitle); - } - - public static IQueryable Search(this IQueryable queryable, - string searchQuery, int userId, AgeRestriction userRating) - { - return queryable - .Where(rl => rl.AppUserId == userId || rl.Promoted) - .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) - .RestrictAgainstAgeRestriction(userRating) - .OrderBy(s => s.NormalizedTitle); - } - - public static IQueryable Search(this IQueryable queryable, - string searchQuery, int userId, IEnumerable libraryIds) - { - return queryable - .Where(l => libraryIds.Contains(l.Id)) - .Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%")) - .IsRestricted(QueryContext.Search) - .AsSplitQuery() - .OrderBy(l => l.Name.ToLower()); - } - - public static IQueryable SearchPeople(this IQueryable queryable, - string searchQuery, IEnumerable seriesIds) - { - // Get people from SeriesMetadata - var peopleFromSeriesMetadata = queryable - .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.People.Select(sp => sp.Person)) - .Where(p => - EF.Functions.Like(p.Name, $"%{searchQuery}%") || - p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) - ); - - var peopleFromChapterPeople = queryable - .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.Series.Volumes) - .SelectMany(v => v.Chapters) - .SelectMany(ch => ch.People.Select(cp => cp.Person)) - .Where(p => - EF.Functions.Like(p.Name, $"%{searchQuery}%") || - p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) - ); - - // Combine both queries and ensure distinct results - return peopleFromSeriesMetadata - .Union(peopleFromChapterPeople) - .Select(p => p) - .OrderBy(p => p.NormalizedName); - } - - public static IQueryable SearchGenres(this IQueryable queryable, - string searchQuery, IEnumerable seriesIds) - { - return queryable - .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) - .Distinct() - .OrderBy(t => t.NormalizedTitle); - } - - public static IQueryable SearchTags(this IQueryable queryable, - string searchQuery, IEnumerable seriesIds) - { - return queryable - .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) - .AsSplitQuery() - .Distinct() - .OrderBy(t => t.NormalizedTitle); - } -} diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index ad51a4a62..a2f8877fd 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -9,12 +9,12 @@ using Kavita.Common; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions.Filtering; + #nullable enable public static class SeriesFilter { private const float FloatingPointTolerance = 0.001f; - public static IQueryable HasLanguage(this IQueryable queryable, bool condition, FilterComparison comparison, IList languages) { @@ -23,7 +23,7 @@ public static class SeriesFilter switch (comparison) { case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.Language.Equals(languages[0])); + return queryable.Where(s => s.Metadata.Language.Equals(languages.First())); case FilterComparison.Contains: return queryable.Where(s => languages.Contains(s.Metadata.Language)); case FilterComparison.MustContains: @@ -31,9 +31,9 @@ public static class SeriesFilter case FilterComparison.NotContains: return queryable.Where(s => !languages.Contains(s.Metadata.Language)); case FilterComparison.NotEqual: - return queryable.Where(s => !s.Metadata.Language.Equals(languages[0])); + return queryable.Where(s => !s.Metadata.Language.Equals(languages.First())); case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%")); + return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages.First()}%")); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -44,7 +44,6 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); } @@ -73,8 +72,6 @@ public static class SeriesFilter return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear); case FilterComparison.IsNotInLast: return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.ReleaseYear == 0); case FilterComparison.Matches: case FilterComparison.Contains: case FilterComparison.NotContains: @@ -90,18 +87,14 @@ public static class SeriesFilter public static IQueryable HasRating(this IQueryable queryable, bool condition, - FilterComparison comparison, float rating, int userId) + FilterComparison comparison, int rating, int userId) { if (rating < 0 || !condition || userId <= 0) return queryable; - // AppUserRating stores a 5-digit number. - rating = Math.Clamp(rating, 0f, 5f); - - switch (comparison) { case FilterComparison.Equal: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId)); + return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) < FloatingPointTolerance && r.AppUserId == userId)); case FilterComparison.GreaterThan: return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId)); case FilterComparison.GreaterThanEqual: @@ -110,13 +103,10 @@ public static class SeriesFilter return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId)); case FilterComparison.LessThanEqual: return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId)); case FilterComparison.Contains: case FilterComparison.Matches: case FilterComparison.NotContains: + case FilterComparison.NotEqual: case FilterComparison.BeginsWith: case FilterComparison.EndsWith: case FilterComparison.IsBefore: @@ -135,7 +125,7 @@ public static class SeriesFilter { if (!condition || ratings.Count == 0) return queryable; - var firstRating = ratings[0]; + var firstRating = ratings.First(); switch (comparison) { case FilterComparison.Equal: @@ -162,13 +152,11 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); } } - public static IQueryable HasAverageReadTime(this IQueryable queryable, bool condition, FilterComparison comparison, int avgReadTime) { @@ -177,17 +165,17 @@ public static class SeriesFilter switch (comparison) { case FilterComparison.NotEqual: - return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead != avgReadTime); case FilterComparison.Equal: - return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead == avgReadTime); case FilterComparison.GreaterThan: - return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead > avgReadTime); case FilterComparison.GreaterThanEqual: - return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead >= avgReadTime); case FilterComparison.LessThan: - return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead < avgReadTime); case FilterComparison.LessThanEqual: - return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + return queryable.Where(s => s.AvgHoursToRead <= avgReadTime); case FilterComparison.Contains: case FilterComparison.Matches: case FilterComparison.NotContains: @@ -198,7 +186,6 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -210,7 +197,7 @@ public static class SeriesFilter { if (!condition || pubStatues.Count == 0) return queryable; - var firstStatus = pubStatues[0]; + var firstStatus = pubStatues.First(); switch (comparison) { case FilterComparison.Equal: @@ -233,7 +220,6 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.Matches: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -251,178 +237,54 @@ public static class SeriesFilter { if (!condition) return queryable; - var subQuery = queryable - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - Percentage = s.Progress - .Where(p => p != null && p.AppUserId == userId) - .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f - }) - .AsSplitQuery(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.IsEmpty: - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasAverageRating(this IQueryable queryable, bool condition, - FilterComparison comparison, float rating) - { - if (!condition) return queryable; - - var subQuery = queryable - .Where(s => s.ExternalSeriesMetadata != null) - .Include(s => s.ExternalSeriesMetadata) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - AverageRating = s.ExternalSeriesMetadata.AverageExternalRating - }) - .AsSplitQuery() - .AsQueryable(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - /// - /// HasReadingDate but used to filter where last reading point was TODAY() - timeDeltaDays. This allows the user - /// to build smart filters "Haven't read in a month" - /// - public static IQueryable HasReadLast(this IQueryable queryable, bool condition, - FilterComparison comparison, int timeDeltaDays, int userId) - { - if (!condition || timeDeltaDays == 0) return queryable; - var subQuery = queryable .Include(s => s.Progress) - .Where(s => s.Progress.Any()) + .Where(s => s.Progress != null) .Select(s => new { - SeriesId = s.Id, - SeriesName = s.Name, - MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) - .Select(p => (DateTime?) p.LastModified) - .DefaultIfEmpty() - .Max() + Series = s, + Percentage = ((float) s.Progress + .Where(p => p != null && p.AppUserId == userId) + .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0) * 100) }) - .Where(s => s.MaxDate != null) - .AsSplitQuery() .AsEnumerable(); - var date = DateTime.Now.AddDays(-timeDeltaDays); - switch (comparison) { case FilterComparison.Equal: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); + subQuery = subQuery.Where(s => Math.Abs(s.Percentage - readProgress) < FloatingPointTolerance); break; - case FilterComparison.IsAfter: case FilterComparison.GreaterThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); + subQuery = subQuery.Where(s => s.Percentage > readProgress); break; case FilterComparison.GreaterThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); + subQuery = subQuery.Where(s => s.Percentage >= readProgress); break; - case FilterComparison.IsBefore: case FilterComparison.LessThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); + subQuery = subQuery.Where(s => s.Percentage < readProgress); break; case FilterComparison.LessThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); + subQuery = subQuery.Where(s => s.Percentage <= readProgress); break; case FilterComparison.NotEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); + subQuery = subQuery.Where(s => Math.Abs(s.Percentage - readProgress) > FloatingPointTolerance); break; case FilterComparison.Matches: case FilterComparison.Contains: case FilterComparison.NotContains: case FilterComparison.BeginsWith: case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); } - var ids = subQuery.Select(s => s.SeriesId); + var ids = subQuery.Select(s => s.Series.Id).ToList(); return queryable.Where(s => ids.Contains(s.Id)); } @@ -433,18 +295,16 @@ public static class SeriesFilter var subQuery = queryable .Include(s => s.Progress) - .Where(s => s.Progress.Any()) + .Where(s => s.Progress != null) .Select(s => new { - SeriesId = s.Id, - SeriesName = s.Name, + Series = s, MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) .Select(p => (DateTime?) p.LastModified) .DefaultIfEmpty() .Max() }) .Where(s => s.MaxDate != null) - .AsSplitQuery() .AsEnumerable(); switch (comparison) @@ -477,20 +337,19 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); } - var ids = subQuery.Select(s => s.SeriesId); + var ids = subQuery.Select(s => s.Series.Id).ToList(); return queryable.Where(s => ids.Contains(s.Id)); } public static IQueryable HasTags(this IQueryable queryable, bool condition, FilterComparison comparison, IList tags) { - if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; + if (!condition || tags.Count == 0) return queryable; switch (comparison) { @@ -509,8 +368,6 @@ public static class SeriesFilter queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Tags.Count == 0); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -529,48 +386,6 @@ public static class SeriesFilter } public static IQueryable HasPeople(this IQueryable queryable, bool condition, - FilterComparison comparison, IList people, PersonRole role) - { - if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role)); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role)); - case FilterComparison.MustContains: - var queries = new List>() - { - queryable - }; - queries.AddRange(people.Select(personId => - queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - // Ensure no person with the given role exists - return queryable.Where(s => s.Metadata.People.All(p => p.Role != role)); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - throw new KavitaException($"{comparison} not applicable for Series.People"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPeopleLegacy(this IQueryable queryable, bool condition, FilterComparison comparison, IList people) { if (!condition || people.Count == 0) return queryable; @@ -579,20 +394,19 @@ public static class SeriesFilter { case FilterComparison.Equal: case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId))); + return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.Id))); case FilterComparison.NotEqual: case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId))); + return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.Id))); case FilterComparison.MustContains: // Deconstruct and do a Union of a bunch of where statements since this doesn't translate var queries = new List>() { queryable }; - queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == gId)))); + queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.Id == gId)))); return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -613,7 +427,7 @@ public static class SeriesFilter public static IQueryable HasGenre(this IQueryable queryable, bool condition, FilterComparison comparison, IList genres) { - if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; + if (!condition || genres.Count == 0) return queryable; switch (comparison) { @@ -632,8 +446,6 @@ public static class SeriesFilter queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Genres.Count == 0); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -676,7 +488,6 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.Format"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -684,30 +495,27 @@ public static class SeriesFilter } public static IQueryable HasCollectionTags(this IQueryable queryable, bool condition, - FilterComparison comparison, IList collectionTags, IList collectionSeries) + FilterComparison comparison, IList collectionTags) { - if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; - + if (!condition || collectionTags.Count == 0) return queryable; switch (comparison) { case FilterComparison.Equal: case FilterComparison.Contains: - return queryable.Where(s => collectionSeries.Contains(s.Id)); + return queryable.Where(s => s.Metadata.CollectionTags.Any(t => collectionTags.Contains(t.Id))); case FilterComparison.NotContains: case FilterComparison.NotEqual: - return queryable.Where(s => !collectionSeries.Contains(s.Id)); + return queryable.Where(s => !s.Metadata.CollectionTags.Any(t => collectionTags.Contains(t.Id))); case FilterComparison.MustContains: - // // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate var queries = new List>() { queryable }; - queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id)))); + queries.AddRange(collectionTags.Select(gId => queryable.Where(s => s.Metadata.CollectionTags.Any(p => p.Id == gId)))); return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Collections.Count == 0); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -768,7 +576,6 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.Name"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); @@ -792,8 +599,6 @@ public static class SeriesFilter return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%")); case FilterComparison.NotEqual: return queryable.Where(s => s.Metadata.Summary != queryString); - case FilterComparison.IsEmpty: - return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary)); case FilterComparison.NotContains: case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: @@ -841,7 +646,6 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); @@ -918,7 +722,6 @@ public static class SeriesFilter case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: case FilterComparison.MustContains: - case FilterComparison.IsEmpty: throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs index d6c7ff77d..52c41c4ee 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs @@ -1,10 +1,7 @@ using System.Linq; using API.DTOs.Filtering; using API.Entities; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable +using API.Extensions.QueryExtensions; public static class SeriesSort { @@ -32,11 +29,8 @@ public static class SeriesSort SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, sortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, sortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) - .Select(p => p.LastModified) // TODO: Migrate this to UTC + .Select(p => p.LastModified) .Max(), sortOptions), - SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings - .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), sortOptions), - SortField.Random => query.DoOrderBy(s => EF.Functions.Random(), sortOptions), _ => query }; diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index bfc585455..006364ffb 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -1,11 +1,9 @@ using System.Linq; using API.Data.Repositories; using API.Entities; -using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; -#nullable enable /// /// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern @@ -20,23 +18,6 @@ public static class IncludesExtensions queryable = queryable.Include(c => c.SeriesMetadatas); } - if (includes.HasFlag(CollectionTagIncludes.SeriesMetadataWithSeries)) - { - queryable = queryable.Include(c => c.SeriesMetadatas).ThenInclude(s => s.Series); - } - - return queryable.AsSplitQuery(); - } - - public static IQueryable Includes(this IQueryable queryable, - CollectionIncludes includes) - { - if (includes.HasFlag(CollectionIncludes.Series)) - { - queryable = queryable.Include(c => c.Items); - } - - return queryable.AsSplitQuery(); } @@ -54,67 +35,6 @@ public static class IncludesExtensions .Include(c => c.Files); } - if (includes.HasFlag(ChapterIncludes.People)) - { - queryable = queryable - .Include(c => c.People) - .ThenInclude(cp => cp.Person); - } - - if (includes.HasFlag(ChapterIncludes.Genres)) - { - queryable = queryable - .Include(c => c.Genres); - } - - if (includes.HasFlag(ChapterIncludes.Tags)) - { - queryable = queryable - .Include(c => c.Tags); - } - - if (includes.HasFlag(ChapterIncludes.ExternalReviews)) - { - queryable = queryable - .Include(c => c.ExternalReviews); - } - - if (includes.HasFlag(ChapterIncludes.ExternalRatings)) - { - queryable = queryable - .Include(c => c.ExternalRatings); - } - - return queryable.AsSplitQuery(); - } - - public static IQueryable Includes(this IQueryable queryable, - VolumeIncludes includes) - { - if (includes.HasFlag(VolumeIncludes.Files)) - { - queryable = queryable - .Include(vol => vol.Chapters) - .ThenInclude(c => c.Files); - } else if (includes.HasFlag(VolumeIncludes.Chapters)) - { - queryable = queryable - .Include(vol => vol.Chapters); - } - - if (includes.HasFlag(VolumeIncludes.People)) - { - queryable = queryable - .Include(vol => vol.Chapters) - .ThenInclude(c => c.People); - } - - if (includes.HasFlag(VolumeIncludes.Tags)) - { - queryable = queryable - .Include(vol => vol.Chapters) - .ThenInclude(c => c.Tags); - } return queryable.AsSplitQuery(); } @@ -136,7 +56,7 @@ public static class IncludesExtensions { query = query .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters.OrderBy(c => c.SortOrder)); + .ThenInclude(v => v.Chapters); } if (includeFlags.HasFlag(SeriesIncludes.Related)) @@ -146,45 +66,19 @@ public static class IncludesExtensions .Include(s => s.RelationOf); } - if (includeFlags.HasFlag(SeriesIncludes.ExternalReviews)) - { - query = query - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(s => s.ExternalReviews); - } - - if (includeFlags.HasFlag(SeriesIncludes.ExternalRatings)) - { - query = query - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(s => s.ExternalRatings); - } - - if (includeFlags.HasFlag(SeriesIncludes.ExternalMetadata)) - { - query = query - .Include(s => s.ExternalSeriesMetadata); - } - - if (includeFlags.HasFlag(SeriesIncludes.ExternalRecommendations)) - { - query = query - .Include(s => s.ExternalSeriesMetadata) - .ThenInclude(s => s.ExternalRecommendations); - } - if (includeFlags.HasFlag(SeriesIncludes.Metadata)) { - query = query + query = query.Include(s => s.Metadata) + .ThenInclude(m => m.CollectionTags.OrderBy(g => g.NormalizedTitle)) .Include(s => s.Metadata) .ThenInclude(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(s => s.Metadata) .ThenInclude(m => m.People) - .ThenInclude(smp => smp.Person) .Include(s => s.Metadata) .ThenInclude(m => m.Tags.OrderBy(g => g.NormalizedTitle)); } + return query.AsSplitQuery(); } @@ -218,9 +112,7 @@ public static class IncludesExtensions if (includeFlags.HasFlag(AppUserIncludes.UserPreferences)) { - query = query - .Include(u => u.UserPreferences) - .ThenInclude(p => p.Theme); + query = query.Include(u => u.UserPreferences); } if (includeFlags.HasFlag(AppUserIncludes.WantToRead)) @@ -260,17 +152,6 @@ public static class IncludesExtensions query = query.Include(u => u.ExternalSources); } - if (includeFlags.HasFlag(AppUserIncludes.Collections)) - { - query = query.Include(u => u.Collections) - .ThenInclude(c => c.Items); - } - - if (includeFlags.HasFlag(AppUserIncludes.ChapterRatings)) - { - query = query.Include(u => u.ChapterRatings); - } - return query.AsSplitQuery(); } @@ -291,55 +172,4 @@ public static class IncludesExtensions return queryable.AsSplitQuery(); } - - public static IQueryable Includes(this IQueryable query, LibraryIncludes includeFlags) - { - if (includeFlags.HasFlag(LibraryIncludes.Folders)) - { - query = query.Include(l => l.Folders); - } - - if (includeFlags.HasFlag(LibraryIncludes.FileTypes)) - { - query = query.Include(l => l.LibraryFileTypes); - } - - if (includeFlags.HasFlag(LibraryIncludes.Series)) - { - query = query.Include(l => l.Series); - } - - if (includeFlags.HasFlag(LibraryIncludes.AppUser)) - { - query = query.Include(l => l.AppUsers); - } - - if (includeFlags.HasFlag(LibraryIncludes.ExcludePatterns)) - { - query = query.Include(l => l.LibraryExcludePatterns); - } - - return query.AsSplitQuery(); - } - - public static IQueryable Includes(this IQueryable queryable, PersonIncludes includeFlags) - { - - if (includeFlags.HasFlag(PersonIncludes.Aliases)) - { - queryable = queryable.Include(p => p.Aliases); - } - - if (includeFlags.HasFlag(PersonIncludes.ChapterPeople)) - { - queryable = queryable.Include(p => p.ChapterPeople); - } - - if (includeFlags.HasFlag(PersonIncludes.SeriesPeople)) - { - queryable = queryable.Include(p => p.SeriesMetadataPeople); - } - - return queryable; - } } diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index ef2af721f..eca302203 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -5,23 +5,16 @@ using System.Linq.Expressions; using System.Threading.Tasks; using API.Data.Misc; using API.Data.Repositories; -using API.DTOs; using API.DTOs.Filtering; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Entities.Enums; -using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; -#nullable enable public static class QueryableExtensions { - private const float DefaultTolerance = 0.001f; - public static Task GetUserAgeRestriction(this DbSet queryable, int userId) { if (userId < 1) @@ -85,6 +78,7 @@ public static class QueryableExtensions .Include(l => l.AppUsers) .Where(lib => lib.AppUsers.Any(user => user.Id == userId)) .IsRestricted(queryContext) + .AsNoTracking() .AsSplitQuery() .Select(lib => lib.Id); } @@ -117,98 +111,18 @@ public static class QueryableExtensions return condition ? queryable.Where(predicate) : queryable; } - - public static IQueryable WhereGreaterThan(this IQueryable source, - Expression> selector, - float value) + public static IQueryable WhereLike(this IQueryable queryable, bool condition, Expression> propertySelector, string searchQuery) + where T : class { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; + if (!condition || string.IsNullOrEmpty(searchQuery)) return queryable; - var greaterThanExpression = Expression.GreaterThan(propertyAccess, Expression.Constant(value)); - var lambda = Expression.Lambda>(greaterThanExpression, parameter); + var method = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) }); + var dbFunctions = typeof(EF).GetMethod(nameof(EF.Functions))?.Invoke(null, null); + var searchExpression = Expression.Constant($"%{searchQuery}%"); + var likeExpression = Expression.Call(method, Expression.Constant(dbFunctions), propertySelector.Body, searchExpression); + var lambda = Expression.Lambda>(likeExpression, propertySelector.Parameters[0]); - return source.Where(lambda); - } - - public static IQueryable WhereGreaterThanOrEqual(this IQueryable source, - Expression> selector, - float value) - { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; - - var greaterThanExpression = Expression.GreaterThanOrEqual(propertyAccess, Expression.Constant(value)); - var lambda = Expression.Lambda>(greaterThanExpression, parameter); - - return source.Where(lambda); - } - - public static IQueryable WhereLessThan(this IQueryable source, - Expression> selector, - float value) - { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; - - var lessThanExpression = Expression.LessThan(propertyAccess, Expression.Constant(value)); - var lambda = Expression.Lambda>(lessThanExpression, parameter); - - return source.Where(lambda); - } - - public static IQueryable WhereLessThanOrEqual(this IQueryable source, - Expression> selector, - float value) - { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; - - var lessThanOrEqualExpression = Expression.LessThanOrEqual(propertyAccess, Expression.Constant(value)); - var lambda = Expression.Lambda>(lessThanOrEqualExpression, parameter); - - return source.Where(lambda); - } - - public static IQueryable WhereEqual(this IQueryable source, - Expression> selector, - float value, - float tolerance = DefaultTolerance) - { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; - - // Absolute difference comparison: Math.Abs(propertyAccess - value) < tolerance - var difference = Expression.Subtract(propertyAccess, Expression.Constant(value)); - var absoluteDifference = Expression.Condition( - Expression.LessThan(difference, Expression.Constant(0f)), - Expression.Negate(difference), - difference); - - var toleranceExpression = Expression.LessThan(absoluteDifference, Expression.Constant(tolerance)); - var lambda = Expression.Lambda>(toleranceExpression, parameter); - - return source.Where(lambda); - } - - public static IQueryable WhereNotEqual(this IQueryable source, - Expression> selector, - float value, - float tolerance = DefaultTolerance) - { - var parameter = selector.Parameters[0]; - var propertyAccess = selector.Body; - - var difference = Expression.Subtract(propertyAccess, Expression.Constant(value)); - var absoluteDifference = Expression.Condition( - Expression.LessThan(difference, Expression.Constant(0f)), - Expression.Negate(difference), - difference); - - var toleranceExpression = Expression.GreaterThan(absoluteDifference, Expression.Constant(tolerance)); - var lambda = Expression.Lambda>(toleranceExpression, parameter); - - return source.Where(lambda); + return queryable.Where(lambda); } /// @@ -258,7 +172,6 @@ public static class QueryableExtensions ScrobbleEventSortField.Type => query.OrderByDescending(s => s.ScrobbleEventType), ScrobbleEventSortField.Series => query.OrderByDescending(s => s.Series.NormalizedName), ScrobbleEventSortField.IsProcessed => query.OrderByDescending(s => s.IsProcessed), - ScrobbleEventSortField.ScrobbleEventFilter => query.OrderByDescending(s => s.ScrobbleEventType), _ => query }; } @@ -271,32 +184,10 @@ public static class QueryableExtensions ScrobbleEventSortField.Type => query.OrderBy(s => s.ScrobbleEventType), ScrobbleEventSortField.Series => query.OrderBy(s => s.Series.NormalizedName), ScrobbleEventSortField.IsProcessed => query.OrderBy(s => s.IsProcessed), - ScrobbleEventSortField.ScrobbleEventFilter => query.OrderBy(s => s.ScrobbleEventType), _ => query }; } - public static IQueryable SortBy(this IQueryable query, PersonSortOptions? sort) - { - if (sort == null) - { - return query.OrderBy(p => p.Name); - } - - return sort.SortField switch - { - PersonSortField.Name when sort.IsAscending => query.OrderBy(p => p.Name), - PersonSortField.Name => query.OrderByDescending(p => p.Name), - PersonSortField.SeriesCount when sort.IsAscending => query.OrderBy(p => p.SeriesMetadataPeople.Count), - PersonSortField.SeriesCount => query.OrderByDescending(p => p.SeriesMetadataPeople.Count), - PersonSortField.ChapterCount when sort.IsAscending => query.OrderBy(p => p.ChapterPeople.Count), - PersonSortField.ChapterCount => query.OrderByDescending(p => p.ChapterPeople.Count), - _ => query.OrderBy(p => p.Name) - }; - - - } - /// /// Performs either OrderBy or OrderByDescending on the given query based on the value of SortOptions.IsAscending. /// @@ -308,21 +199,4 @@ public static class QueryableExtensions { return sortOptions.IsAscending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector); } - - public static IQueryable FilterMatchState(this IQueryable query, MatchStateOption stateOption) - { - return stateOption switch - { - MatchStateOption.All => query, - MatchStateOption.Matched => query - .Include(s => s.ExternalSeriesMetadata) - .Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted), - MatchStateOption.NotMatched => query. - Include(s => s.ExternalSeriesMetadata) - .Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted && !s.DontMatch), - MatchStateOption.Error => query.Where(s => s.IsBlacklisted && !s.DontMatch), - MatchStateOption.DontMatch => query.Where(s => s.DontMatch), - _ => query - }; - } } diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index e0738bdf3..866382587 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -1,13 +1,9 @@ -using System; -using System.Linq; +using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; namespace API.Extensions.QueryExtensions; -#nullable enable /// /// Responsible for restricting Entities based on an AgeRestriction @@ -27,82 +23,32 @@ public static class RestrictByAgeExtensions return q; } - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(s => s.SeriesMetadata.AgeRating <= restriction.AgeRating); - - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.SeriesMetadata.AgeRating != AgeRating.Unknown); - } - - return q; - } - - - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(chapter => chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating); - - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.Volume.Series.Metadata.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating); - - if (!restriction.IncludeUnknowns) - { - return q.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown); - } - - return q; - } - - - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; if (restriction.IncludeUnknowns) { - return queryable.Where(c => c.Items.All(sm => - sm.Metadata.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => c.Items.All(sm => - sm.Metadata.AgeRating <= restriction.AgeRating && sm.Metadata.AgeRating > AgeRating.Unknown)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } - /// - /// Returns all Genres where any of the linked Series/Chapters are less than or equal to restriction age rating - /// - /// - /// - /// public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; if (restriction.IncludeUnknowns) { - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) - ); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) @@ -111,15 +57,12 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) - ); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) @@ -128,15 +71,12 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => - c.SeriesMetadataPeople.Any(sm => sm.SeriesMetadata.AgeRating <= restriction.AgeRating) || - c.ChapterPeople.Any(cp => cp.Chapter.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => - c.SeriesMetadataPeople.Any(sm => sm.SeriesMetadata.AgeRating <= restriction.AgeRating && sm.SeriesMetadata.AgeRating != AgeRating.Unknown) || - c.ChapterPeople.Any(cp => cp.Chapter.AgeRating <= restriction.AgeRating && cp.Chapter.AgeRating != AgeRating.Unknown) - ); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs deleted file mode 100644 index 9ec1b8621..000000000 --- a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using API.Entities; -using API.Entities.Person; - -namespace API.Extensions.QueryExtensions; - -public static class RestrictByLibraryExtensions -{ - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(p => - p.ChapterPeople.Any(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)) || - p.SeriesMetadataPeople.Any(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId))); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(cp => userLibs.Contains(cp.Volume.Series.LibraryId)); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId)); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)); - } -} diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index 01ae718c7..5223f3120 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -1,10 +1,12 @@ -using System.Linq; +#nullable enable +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using API.Comparators; using API.Entities; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; -#nullable enable public static class SeriesExtensions { @@ -16,26 +18,14 @@ public static class SeriesExtensions /// This is under the assumption that the Volume already has a Cover Image calculated and set public static string? GetCoverImage(this Series series) { - var volumes = (series.Volumes ?? []) - .OrderBy(v => v.MinNumber, ChapterSortComparerDefaultLast.Default) + var volumes = (series.Volumes ?? new List()) + .OrderBy(v => v.Number, ChapterSortComparer.Default) .ToList(); var firstVolume = volumes.GetCoverImage(series.Format); if (firstVolume == null) return null; - // If first volume here is specials, move to the next as specials should almost always be last. - if (firstVolume.MinNumber.Is(Parser.SpecialVolumeNumber) && volumes.Count > 1) - { - firstVolume = volumes[1]; - } - - // If the first volume is 0, then use Volume 1 - if (firstVolume.MinNumber.Is(0f) && volumes.Count > 1) - { - firstVolume = volumes[1]; - } - var chapters = firstVolume.Chapters - .OrderBy(c => c.SortOrder) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial)) @@ -44,42 +34,32 @@ public static class SeriesExtensions } // just volumes - if (volumes.TrueForAll(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber))) + if (volumes.TrueForAll(v => $"{v.Number}" != Parser.DefaultVolume)) { return firstVolume.CoverImage; } // If we have loose leaf chapters // if loose leaf chapters AND volumes, just return first volume - if (volumes.Count >= 1 && volumes[0].MinNumber.IsNot(Parser.LooseLeafVolumeNumber)) + if (volumes.Count >= 1 && $"{volumes[0].Number}" != Parser.DefaultVolume) { - var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) - .SelectMany(c => c.Chapters.Where(c2 => !c2.IsSpecial)) - .OrderBy(c => c.SortOrder) + var looseLeafChapters = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + .SelectMany(c => c.Chapters.Where(c => !c.IsSpecial)) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); - - if (looseLeafChapters.Count > 0 && volumes[0].MinNumber > looseLeafChapters[0].MinNumber) + if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].Number) > looseLeafChapters[0].Number.AsFloat()) { - var first = looseLeafChapters.Find(c => c.SortOrder.Is(1f)); - if (first != null) return first.CoverImage; return looseLeafChapters[0].CoverImage; } return firstVolume.CoverImage; } - var chpts = volumes - .First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) - .Chapters - .Where(c => !c.IsSpecial) - .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) - .ToList(); + var firstLooseLeafChapter = volumes + .Where(v => $"{v.Number}" == Parser.DefaultVolume) + .SelectMany(v => v.Chapters) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) + .FirstOrDefault(c => !c.IsSpecial); - var exactlyChapter1 = chpts.Find(c => c.MinNumber.Is(1f)); - if (exactlyChapter1 != null) - { - return exactlyChapter1.CoverImage; - } - - return chpts.FirstOrDefault()?.CoverImage ?? firstVolume.CoverImage; + return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage; } } diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index 28419921a..ae65ffe38 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -1,32 +1,12 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; namespace API.Extensions; -#nullable enable public static class StringExtensions { - private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)", - RegexOptions.ExplicitCapture | RegexOptions.Compiled, - Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - - public static string Sanitize(this string input) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - // Remove all newline and control characters - var sanitized = input - .Replace(Environment.NewLine, string.Empty) - .Replace("\n", string.Empty) - .Replace("\r", string.Empty); - - // Optionally remove other potentially unwanted characters - sanitized = Regex.Replace(sanitized, @"[^\u0020-\u007E]", string.Empty); // Removes non-printable ASCII - - return sanitized.Trim(); // Trim any leading/trailing whitespace - } + private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)", + RegexOptions.ExplicitCapture | RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); public static string SentenceCase(this string value) { @@ -40,16 +20,17 @@ public static class StringExtensions /// public static string ToNormalized(this string? value) { - return string.IsNullOrEmpty(value) ? string.Empty : Services.Tasks.Scanner.Parser.Parser.Normalize(value); + if (string.IsNullOrEmpty(value)) return string.Empty; + return Services.Tasks.Scanner.Parser.Parser.Normalize(value); } - public static float AsFloat(this string? value, float defaultValue = 0.0f) + public static float AsFloat(this string value) { - return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture); + return float.Parse(value, CultureInfo.InvariantCulture); } - public static double AsDouble(this string? value, double defaultValue = 0.0f) + public static double AsDouble(this string value) { - return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture); + return double.Parse(value, CultureInfo.InvariantCulture); } } diff --git a/API/Extensions/VersionExtensions.cs b/API/Extensions/VersionExtensions.cs deleted file mode 100644 index 1877b48b1..000000000 --- a/API/Extensions/VersionExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace API.Extensions; - -public static class VersionExtensions -{ - public static bool CompareWithoutRevision(this Version v1, Version v2) - { - if (v1.Major != v2.Major) - return v1.Major == v2.Major; - if (v1.Minor != v2.Minor) - return v1.Minor == v2.Minor; - if (v1.Build != v2.Build) - return v1.Build == v2.Build; - return true; - } -} diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index a5febb1ff..0d42b15e8 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -1,16 +1,10 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; -using API.Comparators; -using API.DTOs; using API.Entities; using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; -#nullable enable public static class VolumeListExtensions { @@ -25,68 +19,18 @@ public static class VolumeListExtensions { if (volumes == null) throw new ArgumentException("Volumes cannot be null"); - if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf) + if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) { - return volumes.MinBy(x => x.MinNumber); + return volumes.MinBy(x => x.Number); } - if (volumes.HasAnyNonLooseLeafVolumes()) + + if (volumes.Any(x => x.Number != 0)) { - return volumes.FirstNonLooseLeafOrDefault(); + return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); } // We only have 1 volume of chapters, we need to be cautious if there are specials, as we don't want to order them first - return volumes.MinBy(x => x.MinNumber); - } - - /// - /// If the collection of volumes has any non-loose leaf volumes - /// - /// - /// - public static bool HasAnyNonLooseLeafVolumes(this IEnumerable volumes) - { - return volumes.Any(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); - } - - /// - /// Returns first non-loose leaf volume - /// - /// - /// - public static Volume? FirstNonLooseLeafOrDefault(this IEnumerable volumes) - { - return volumes.OrderBy(x => x.MinNumber, ChapterSortComparerDefaultLast.Default) - .FirstOrDefault(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); - } - - /// - /// Returns the first (and only) loose leaf volume or null if none - /// - /// - /// - public static Volume? GetLooseLeafVolumeOrDefault(this IEnumerable volumes) - { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); - } - - /// - /// Returns the first (and only) special volume or null if none - /// - /// - /// - public static Volume? GetSpecialVolumeOrDefault(this IEnumerable volumes) - { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.SpecialVolumeNumber)); - } - - public static IEnumerable WhereNotLooseLeaf(this IEnumerable volumes) - { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); - } - - public static IEnumerable WhereLooseLeaf(this IEnumerable volumes) - { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.MinBy(x => x.Number); } } diff --git a/API/Extensions/ZipArchiveExtensions.cs b/API/Extensions/ZipArchiveExtensions.cs index 8ed338e57..89a083490 100644 --- a/API/Extensions/ZipArchiveExtensions.cs +++ b/API/Extensions/ZipArchiveExtensions.cs @@ -3,7 +3,6 @@ using System.IO.Compression; using System.Linq; namespace API.Extensions; -#nullable enable public static class ZipArchiveExtensions { diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index bb7511c64..9585b92a9 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,45 +1,31 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using API.Data.Migrations; using API.DTOs; using API.DTOs.Account; -using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Dashboard; using API.DTOs.Device; -using API.DTOs.Email; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.KavitaPlus.Metadata; using API.DTOs.MediaErrors; using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Progress; using API.DTOs.Reader; using API.DTOs.ReadingLists; -using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.DTOs.Search; using API.DTOs.SeriesDetail; using API.DTOs.Settings; using API.DTOs.SideNav; -using API.DTOs.Stats; using API.DTOs.Theme; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; using API.Entities.Scrobble; using API.Extensions.QueryExtensions.Filtering; using API.Helpers.Converters; -using API.Services; using AutoMapper; using CollectionTag = API.Entities.CollectionTag; -using EmailHistory = API.Entities.EmailHistory; -using ExternalSeriesMetadata = API.Entities.Metadata.ExternalSeriesMetadata; using MediaError = API.Entities.MediaError; using PublicationStatus = API.Entities.Enums.PublicationStatus; using SiteTheme = API.Entities.SiteTheme; @@ -58,19 +44,12 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.Bookmark.ChapterId)) .ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series)); CreateMap(); - CreateMap() - .ForMember(dest => dest.Number, - opt => opt.MapFrom(src => (int) src.MinNumber)) - .ForMember(dest => dest.Chapters, - opt => opt.MapFrom(src => src.Chapters.OrderBy(c => c.SortOrder))); + CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap(); - CreateMap() - .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName)) - .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); - CreateMap() - .ForMember(dest => dest.Aliases, opt => opt.MapFrom(src => src.Aliases.Select(s => s.Alias))); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); @@ -99,165 +78,97 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.AppUser.UserName)); - CreateMap() - .ForMember(dest => dest.LibraryId, - opt => - opt.MapFrom(src => src.Series.LibraryId)) - .ForMember(dest => dest.Body, - opt => - opt.MapFrom(src => src.Review)) - .ForMember(dest => dest.Username, - opt => - opt.MapFrom(src => src.AppUser.UserName)); CreateMap() .ForMember(dest => dest.PageNum, opt => opt.MapFrom( src => src.PagesRead)); - CreateMap() - // Map Writers - .ForMember(dest => dest.Writers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Writer) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map CoverArtists - .ForMember(dest => dest.CoverArtists, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.CoverArtist) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Publishers - .ForMember(dest => dest.Publishers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Publisher) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Characters - .ForMember(dest => dest.Characters, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Character) - .OrderBy(cp => cp.OrderWeight) - .Select(cp => cp.Person))) - // Map Pencillers - .ForMember(dest => dest.Pencillers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Penciller) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Inkers - .ForMember(dest => dest.Inkers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Inker) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Imprints - .ForMember(dest => dest.Imprints, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Imprint) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Colorists - .ForMember(dest => dest.Colorists, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Colorist) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Letterers - .ForMember(dest => dest.Letterers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Letterer) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Editors - .ForMember(dest => dest.Editors, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Editor) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Translators - .ForMember(dest => dest.Translators, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Translator) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Teams - .ForMember(dest => dest.Teams, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Team) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Locations - .ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Location) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Writers, + opt => + opt.MapFrom( + src => src.People.Where(p => p.Role == PersonRole.Writer).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.CoverArtists, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.CoverArtist).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Characters, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Publishers, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Publisher).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Colorists, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Colorist).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Inkers, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Letterers, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Pencillers, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Penciller).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Translators, + opt => + opt.MapFrom(src => + src.People.Where(p => p.Role == PersonRole.Translator).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Editors, + opt => + opt.MapFrom( + src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName))) .ForMember(dest => dest.Genres, opt => opt.MapFrom( src => src.Genres.OrderBy(p => p.NormalizedTitle))) + .ForMember(dest => dest.CollectionTags, + opt => + opt.MapFrom( + src => src.CollectionTags.OrderBy(p => p.NormalizedTitle))) .ForMember(dest => dest.Tags, opt => opt.MapFrom( src => src.Tags.OrderBy(p => p.NormalizedTitle))); - CreateMap() - // Map Writers - .ForMember(dest => dest.Writers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Writer) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map CoverArtists - .ForMember(dest => dest.CoverArtists, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.CoverArtist) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Publishers - .ForMember(dest => dest.Publishers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Publisher) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Characters - .ForMember(dest => dest.Characters, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Character) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Pencillers - .ForMember(dest => dest.Pencillers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Penciller) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Inkers - .ForMember(dest => dest.Inkers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Inker) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Imprints - .ForMember(dest => dest.Imprints, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Imprint) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Colorists - .ForMember(dest => dest.Colorists, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Colorist) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Letterers - .ForMember(dest => dest.Letterers, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Letterer) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Editors - .ForMember(dest => dest.Editors, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Editor) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Translators - .ForMember(dest => dest.Translators, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Translator) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Teams - .ForMember(dest => dest.Teams, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Team) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))) - // Map Locations - .ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.People - .Where(cp => cp.Role == PersonRole.Location) - .Select(cp => cp.Person) - .OrderBy(p => p.NormalizedName))); - + CreateMap() + .ForMember(dest => dest.Writers, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Writer).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.CoverArtists, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.CoverArtist).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Colorists, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Colorist).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Inkers, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Letterers, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Pencillers, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Penciller).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Publishers, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Publisher).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Translators, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Translator).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Characters, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName))) + .ForMember(dest => dest.Editors, + opt => + opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName))); CreateMap() .ForMember(dest => dest.AgeRestriction, @@ -268,30 +179,24 @@ public class AutoMapperProfiles : Profile IncludeUnknowns = src.AgeRestrictionIncludeUnknowns })); - CreateMap() - .ForMember(dest => dest.PreviewUrls, - opt => - opt.MapFrom(src => (src.PreviewUrls ?? string.Empty).Split('|', StringSplitOptions.TrimEntries))); + CreateMap(); CreateMap() .ForMember(dest => dest.Theme, opt => - opt.MapFrom(src => src.Theme)); - - CreateMap() + opt.MapFrom(src => src.Theme)) .ForMember(dest => dest.BookReaderThemeName, opt => - opt.MapFrom(src => src.BookThemeName)); + opt.MapFrom(src => src.BookThemeName)) + .ForMember(dest => dest.BookReaderLayoutMode, + opt => + opt.MapFrom(src => src.BookReaderLayoutMode)); CreateMap(); - CreateMap() - .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)) - .ForMember(dest => dest.OwnerUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); + CreateMap(); CreateMap(); CreateMap(); - CreateMap(); - CreateMap(); CreateMap() .ForMember(dest => dest.SeriesId, @@ -303,13 +208,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.Folders, opt => - opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())) - .ForMember(dest => dest.LibraryFileTypes, - opt => - opt.MapFrom(src => src.LibraryFileTypes.Select(l => l.FileTypeGroup))) - .ForMember(dest => dest.ExcludePatterns, - opt => - opt.MapFrom(src => src.LibraryExcludePatterns.Select(l => l.Pattern))); + opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())); CreateMap() .ForMember(dest => dest.AgeRestriction, @@ -335,58 +234,9 @@ public class AutoMapperProfiles : Profile CreateMap(); CreateMap(); - - // This is for cloning to ensure the records don't get overwritten when setting from SeedData - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - CreateMap() - .ForMember(dest => dest.IsExternal, - opt => - opt.MapFrom(src => true)); - - CreateMap() - .ForMember(dest => dest.BodyJustText, - opt => - opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); - - CreateMap(); - CreateMap() - .ForMember(dest => dest.Series, - opt => - opt.MapFrom(src => src)) - .ForMember(dest => dest.IsMatched, - opt => - opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 - && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue)) - .ForMember(dest => dest.ValidUntilUtc, - opt => opt.MapFrom(src => - src.ExternalSeriesMetadata != null - ? src.ExternalSeriesMetadata.ValidUntilUtc - : DateTime.MinValue)); - - - CreateMap(); - CreateMap() - .ForMember(dest => dest.ToUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); - - CreateMap() - .ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId)) - .ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name)) - .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) - .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); - - CreateMap(); - - CreateMap() - .ForMember(dest => dest.Blacklist, opt => opt.MapFrom(src => src.Blacklist ?? new List())) - .ForMember(dest => dest.Whitelist, opt => opt.MapFrom(src => src.Whitelist ?? new List())) - .ForMember(dest => dest.Overrides, opt => opt.MapFrom(src => src.Overrides ?? new List())) - .ForMember(dest => dest.AgeRatingMappings, opt => opt.MapFrom(src => src.AgeRatingMappings ?? new Dictionary())); - - + // CreateMap() + // .ForMember(dest => dest.SmartFilterEncoded, + // opt => opt.MapFrom(src => src.SmartFilter)); } } diff --git a/API/Helpers/BookSortTitlePrefixHelper.cs b/API/Helpers/BookSortTitlePrefixHelper.cs deleted file mode 100644 index c92df5d65..000000000 --- a/API/Helpers/BookSortTitlePrefixHelper.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace API.Helpers; - -/// -/// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street". -/// -/// This code is performance sensitive -public static class BookSortTitlePrefixHelper -{ - private static readonly Dictionary PrefixLookup; - private static readonly Dictionary> PrefixesByFirstChar; - - static BookSortTitlePrefixHelper() - { - var prefixes = new[] - { - // English - "the", "a", "an", - // Spanish - "el", "la", "los", "las", "un", "una", "unos", "unas", - // French - "le", "la", "les", "un", "une", "des", - // German - "der", "die", "das", "den", "dem", "ein", "eine", "einen", "einer", - // Italian - "il", "lo", "la", "gli", "le", "un", "uno", "una", - // Portuguese - "o", "a", "os", "as", "um", "uma", "uns", "umas", - // Russian (transliterated common ones) - "в", "на", "с", "к", "от", "для", - }; - - // Build lookup structures - PrefixLookup = new Dictionary(prefixes.Length, StringComparer.OrdinalIgnoreCase); - PrefixesByFirstChar = new Dictionary>(); - - foreach (var prefix in prefixes) - { - PrefixLookup[prefix] = 1; - - var firstChar = char.ToLowerInvariant(prefix[0]); - if (!PrefixesByFirstChar.TryGetValue(firstChar, out var list)) - { - list = []; - PrefixesByFirstChar[firstChar] = list; - } - list.Add(prefix); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlySpan GetSortTitle(ReadOnlySpan title) - { - if (title.IsEmpty) return title; - - // Fast detection of script type by first character - var firstChar = title[0]; - - // CJK Unicode ranges - no processing needed for most cases - if ((firstChar >= 0x4E00 && firstChar <= 0x9FFF) || // CJK Unified - (firstChar >= 0x3040 && firstChar <= 0x309F) || // Hiragana - (firstChar >= 0x30A0 && firstChar <= 0x30FF)) // Katakana - { - return title; - } - - var firstSpaceIndex = title.IndexOf(' '); - if (firstSpaceIndex <= 0) return title; - - var potentialPrefix = title.Slice(0, firstSpaceIndex); - - // Fast path: check if first character could match any prefix - firstChar = char.ToLowerInvariant(potentialPrefix[0]); - if (!PrefixesByFirstChar.ContainsKey(firstChar)) - return title; - - // Only do the expensive lookup if first character matches - if (PrefixLookup.ContainsKey(potentialPrefix.ToString())) - { - var remainder = title.Slice(firstSpaceIndex + 1); - return remainder.IsEmpty ? title : remainder; - } - - return title; - } - - /// - /// Removes the sort prefix - /// - /// - /// - public static string GetSortTitle(string title) - { - var result = GetSortTitle(title.AsSpan()); - - return result.ToString(); - } -} diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/API/Helpers/Builders/AppUserBuilder.cs index 7ffac355e..ebe0efa23 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/API/Helpers/Builders/AppUserBuilder.cs @@ -5,7 +5,6 @@ using API.Entities; using Kavita.Common; namespace API.Helpers.Builders; -#nullable enable public class AppUserBuilder : IEntityBuilder { @@ -21,7 +20,7 @@ public class AppUserBuilder : IEntityBuilder ApiKey = HashUtil.ApiKey(), UserPreferences = new AppUserPreferences { - Theme = theme ?? Seed.DefaultThemes.First(), + Theme = theme ?? Seed.DefaultThemes.First() }, ReadingLists = new List(), Bookmarks = new List(), @@ -31,9 +30,10 @@ public class AppUserBuilder : IEntityBuilder Devices = new List(), Id = 0, DashboardStreams = new List(), - SideNavStreams = new List(), - ReadingProfiles = [], + SideNavStreams = new List() }; + _appUser.DashboardStreams = Seed.DefaultStreams.ToList(); + _appUser.SideNavStreams = Seed.DefaultSideNavStreams.ToList(); } public AppUserBuilder WithLibrary(Library library, bool createSideNavStream = false) @@ -62,10 +62,4 @@ public class AppUserBuilder : IEntityBuilder return this; } - public AppUserBuilder WithRole(string role) - { - _appUser.UserRoles ??= new List(); - _appUser.UserRoles.Add(new AppUserRole() {Role = new AppRole() {Name = role}}); - return this; - } } diff --git a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs b/API/Helpers/Builders/AppUserChapterRatingBuilder.cs deleted file mode 100644 index b5deb9228..000000000 --- a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -#nullable enable -using System; -using API.Entities; - -namespace API.Helpers.Builders; - -public class ChapterRatingBuilder : IEntityBuilder -{ - private readonly AppUserChapterRating _rating; - public AppUserChapterRating Build() => _rating; - - public ChapterRatingBuilder(AppUserChapterRating? rating = null) - { - _rating = rating ?? new AppUserChapterRating(); - } - - public ChapterRatingBuilder WithSeriesId(int seriesId) - { - _rating.SeriesId = seriesId; - return this; - } - - public ChapterRatingBuilder WithChapterId(int chapterId) - { - _rating.ChapterId = chapterId; - return this; - } - - public ChapterRatingBuilder WithRating(int rating) - { - _rating.Rating = Math.Clamp(rating, 0, 5); - return this; - } - - public ChapterRatingBuilder WithBody(string body) - { - _rating.Review = body; - return this; - } -} diff --git a/API/Helpers/Builders/AppUserCollectionBuilder.cs b/API/Helpers/Builders/AppUserCollectionBuilder.cs deleted file mode 100644 index e9bdcf977..000000000 --- a/API/Helpers/Builders/AppUserCollectionBuilder.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; - -namespace API.Helpers.Builders; - -public class AppUserCollectionBuilder : IEntityBuilder -{ - private readonly AppUserCollection _collection; - public AppUserCollection Build() => _collection; - - public AppUserCollectionBuilder(string title, bool promoted = false) - { - title = title.Trim(); - _collection = new AppUserCollection() - { - Id = 0, - NormalizedTitle = title.ToNormalized(), - Title = title, - Promoted = promoted, - Summary = string.Empty, - AgeRating = AgeRating.Unknown, - Source = ScrobbleProvider.Kavita, - Items = new List() - }; - } - - public AppUserCollectionBuilder WithSource(ScrobbleProvider provider) - { - _collection.Source = provider; - return this; - } - - - public AppUserCollectionBuilder WithSummary(string summary) - { - _collection.Summary = summary; - return this; - } - - public AppUserCollectionBuilder WithIsPromoted(bool promoted) - { - _collection.Promoted = promoted; - return this; - } - - public AppUserCollectionBuilder WithItem(Series series) - { - _collection.Items ??= new List(); - _collection.Items.Add(series); - return this; - } - - public AppUserCollectionBuilder WithItems(IEnumerable series) - { - _collection.Items ??= new List(); - foreach (var s in series) - { - _collection.Items.Add(s); - } - - return this; - } - - public AppUserCollectionBuilder WithCoverImage(string cover) - { - _collection.CoverImage = cover; - return this; - } - - public AppUserCollectionBuilder WithSourceUrl(string url) - { - _collection.SourceUrl = url; - return this; - } -} diff --git a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs b/API/Helpers/Builders/AppUserReadingProfileBuilder.cs deleted file mode 100644 index 26da5fd86..000000000 --- a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -using API.Entities; -using API.Entities.Enums; -using API.Extensions; - -namespace API.Helpers.Builders; - -public class AppUserReadingProfileBuilder -{ - private readonly AppUserReadingProfile _profile; - - public AppUserReadingProfile Build() => _profile; - - /// - /// The profile's kind will be unless overwritten with - /// - /// - public AppUserReadingProfileBuilder(int userId) - { - _profile = new AppUserReadingProfile - { - AppUserId = userId, - Kind = ReadingProfileKind.User, - SeriesIds = [], - LibraryIds = [] - }; - } - - public AppUserReadingProfileBuilder WithSeries(Series series) - { - _profile.SeriesIds.Add(series.Id); - return this; - } - - public AppUserReadingProfileBuilder WithLibrary(Library library) - { - _profile.LibraryIds.Add(library.Id); - return this; - } - - public AppUserReadingProfileBuilder WithKind(ReadingProfileKind kind) - { - _profile.Kind = kind; - return this; - } - - public AppUserReadingProfileBuilder WithName(string name) - { - _profile.Name = name; - _profile.NormalizedName = name.ToNormalized(); - return this; - } - - -} diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index d9976d92a..c4d6c5785 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using API.Entities; using API.Entities.Enums; -using API.Entities.Person; using API.Services.Tasks.Scanner.Parser; namespace API.Helpers.Builders; -#nullable enable public class ChapterBuilder : IEntityBuilder { @@ -19,29 +16,24 @@ public class ChapterBuilder : IEntityBuilder { _chapter = new Chapter() { - Range = string.IsNullOrEmpty(range) ? number : Parser.RemoveExtensionIfSupported(range), + Range = string.IsNullOrEmpty(range) ? number : range, Title = string.IsNullOrEmpty(range) ? number : range, Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture), - MinNumber = Parser.MinNumberFromRange(number), - MaxNumber = Parser.MaxNumberFromRange(number), - SortOrder = Parser.MinNumberFromRange(number), - Files = [], - Pages = 1, - CreatedUtc = DateTime.UtcNow + Files = new List(), + Pages = 1 }; } public static ChapterBuilder FromParserInfo(ParserInfo info) { var specialTreatment = info.IsSpecialInfo(); - var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters; - var builder = new ChapterBuilder(Parser.DefaultChapter); - - return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters)!) + var specialTitle = specialTreatment ? info.Filename : info.Chapters; + var builder = new ChapterBuilder(Services.Tasks.Scanner.Parser.Parser.DefaultChapter); + return builder.WithNumber(specialTreatment ? Services.Tasks.Scanner.Parser.Parser.DefaultChapter : Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty) .WithRange(specialTreatment ? info.Filename : info.Chapters) - .WithTitle(specialTreatment && info.Format is MangaFormat.Epub or MangaFormat.Pdf + .WithTitle((specialTreatment && info.Format == MangaFormat.Epub) ? info.Title - : specialTitle ?? string.Empty) + : specialTitle) .WithIsSpecial(specialTreatment); } @@ -51,18 +43,9 @@ public class ChapterBuilder : IEntityBuilder return this; } - - private ChapterBuilder WithNumber(string number) + public ChapterBuilder WithNumber(string number) { _chapter.Number = number; - _chapter.MinNumber = Parser.MinNumberFromRange(number); - _chapter.MaxNumber = Parser.MaxNumberFromRange(number); - return this; - } - - public ChapterBuilder WithSortOrder(float order) - { - _chapter.SortOrder = order; return this; } @@ -78,9 +61,9 @@ public class ChapterBuilder : IEntityBuilder return this; } - public ChapterBuilder WithRange(string range) + private ChapterBuilder WithRange(string range) { - _chapter.Range = Parser.RemoveExtensionIfSupported(range); + _chapter.Range = range; return this; } @@ -143,37 +126,4 @@ public class ChapterBuilder : IEntityBuilder _chapter.CreatedUtc = created.ToUniversalTime(); return this; } - - public ChapterBuilder WithPerson(Person person, PersonRole role) - { - _chapter.People ??= new List(); - _chapter.People.Add(new ChapterPeople() - { - Person = person, - Role = role, - Chapter = _chapter, - }); - - return this; - } - - public ChapterBuilder WithTags(IList tags) - { - _chapter.Tags ??= []; - foreach (var tag in tags) - { - _chapter.Tags.Add(tag); - } - return this; - } - - public ChapterBuilder WithGenres(IList genres) - { - _chapter.Genres ??= []; - foreach (var genre in genres) - { - _chapter.Genres.Add(genre); - } - return this; - } } diff --git a/API/Helpers/Builders/CollectionTagBuilder.cs b/API/Helpers/Builders/CollectionTagBuilder.cs new file mode 100644 index 000000000..e46720d79 --- /dev/null +++ b/API/Helpers/Builders/CollectionTagBuilder.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using API.Entities; +using API.Entities.Metadata; +using API.Extensions; + +namespace API.Helpers.Builders; + +public class CollectionTagBuilder : IEntityBuilder +{ + private readonly CollectionTag _collectionTag; + public CollectionTag Build() => _collectionTag; + + public CollectionTagBuilder(string title, bool promoted = false) + { + title = title.Trim(); + _collectionTag = new CollectionTag() + { + Id = 0, + NormalizedTitle = title.ToNormalized(), + Title = title, + Promoted = promoted, + Summary = string.Empty, + SeriesMetadatas = new List() + }; + } + + public CollectionTagBuilder WithId(int id) + { + _collectionTag.Id = id; + return this; + } + + public CollectionTagBuilder WithSummary(string summary) + { + _collectionTag.Summary = summary; + return this; + } + + public CollectionTagBuilder WithIsPromoted(bool promoted) + { + _collectionTag.Promoted = promoted; + return this; + } + + public CollectionTagBuilder WithSeriesMetadata(SeriesMetadata seriesMetadata) + { + _collectionTag.SeriesMetadatas ??= new List(); + _collectionTag.SeriesMetadatas.Add(seriesMetadata); + return this; + } + + public CollectionTagBuilder WithCoverImage(string cover) + { + _collectionTag.CoverImage = cover; + return this; + } +} diff --git a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs b/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs deleted file mode 100644 index e716f5927..000000000 --- a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using API.Entities.Metadata; - -namespace API.Helpers.Builders; - -public class ExternalSeriesMetadataBuilder : IEntityBuilder -{ - private readonly ExternalSeriesMetadata _metadata; - public ExternalSeriesMetadata Build() => _metadata; - - public ExternalSeriesMetadataBuilder() - { - _metadata = new ExternalSeriesMetadata(); - } - - /// - /// -1 for not set, Range 0 - 100 - /// - /// - /// - public ExternalSeriesMetadataBuilder WithAverageExternalRating(int rating) - { - _metadata.AverageExternalRating = Math.Clamp(rating, -1, 100); - return this; - } -} diff --git a/API/Helpers/Builders/GenreBuilder.cs b/API/Helpers/Builders/GenreBuilder.cs index 9b2f1590e..69e68f6c1 100644 --- a/API/Helpers/Builders/GenreBuilder.cs +++ b/API/Helpers/Builders/GenreBuilder.cs @@ -16,14 +16,14 @@ public class GenreBuilder : IEntityBuilder { Title = name.Trim().SentenceCase(), NormalizedTitle = name.ToNormalized(), - Chapters = [], - SeriesMetadatas = [] + Chapters = new List(), + SeriesMetadatas = new List() }; } public GenreBuilder WithSeriesMetadata(SeriesMetadata seriesMetadata) { - _genre.SeriesMetadatas ??= []; + _genre.SeriesMetadatas ??= new List(); _genre.SeriesMetadatas.Add(seriesMetadata); return this; } diff --git a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs b/API/Helpers/Builders/KoreaderBookDtoBuilder.cs deleted file mode 100644 index debbe0347..000000000 --- a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using API.DTOs.Koreader; - -namespace API.Helpers.Builders; - -public class KoreaderBookDtoBuilder : IEntityBuilder -{ - private readonly KoreaderBookDto _dto; - public KoreaderBookDto Build() => _dto; - - public KoreaderBookDtoBuilder(string documentHash) - { - _dto = new KoreaderBookDto() - { - Document = documentHash, - Device = "Kavita" - }; - } - - public KoreaderBookDtoBuilder WithDocument(string documentHash) - { - _dto.Document = documentHash; - return this; - } - - public KoreaderBookDtoBuilder WithProgress(string progress) - { - _dto.Progress = progress; - return this; - } - - public KoreaderBookDtoBuilder WithPercentage(int? pageNum, int pages) - { - _dto.Percentage = (pageNum ?? 0) / (float) pages; - return this; - } - - public KoreaderBookDtoBuilder WithDeviceId(string installId, int userId) - { - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(installId + userId)); - _dto.Device_id = Convert.ToHexString(hash); - return this; - } -} diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 950c5d3d2..ce392e78b 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -20,26 +20,8 @@ public class LibraryBuilder : IEntityBuilder Series = new List(), Folders = new List(), AppUsers = new List(), - AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga, - LibraryFileTypes = new List() + AllowScrobbling = type is LibraryType.Book or LibraryType.Manga }; - - _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Archive - }); - _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Epub - }); - _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Images - }); - _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() - { - FileTypeGroup = FileTypeGroup.Pdf - }); } public LibraryBuilder(Library library) @@ -104,19 +86,7 @@ public class LibraryBuilder : IEntityBuilder return this; } - public LibraryBuilder WithAllowMetadataMatching(bool allow) - { - _library.AllowMetadataMatching = allow; - return this; - } - - public LibraryBuilder WithEnableMetadata(bool enable) - { - _library.EnableMetadata = enable; - return this; - } - - public LibraryBuilder WithAllowScrobbling(bool allowScrobbling) + public LibraryBuilder WIthAllowScrobbling(bool allowScrobbling) { _library.AllowScrobbling = allowScrobbling; return this; diff --git a/API/Helpers/Builders/MangaFileBuilder.cs b/API/Helpers/Builders/MangaFileBuilder.cs index ea3ff0c6d..f07dc4a37 100644 --- a/API/Helpers/Builders/MangaFileBuilder.cs +++ b/API/Helpers/Builders/MangaFileBuilder.cs @@ -2,7 +2,6 @@ using System.IO; using API.Entities; using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; namespace API.Helpers.Builders; @@ -15,12 +14,11 @@ public class MangaFileBuilder : IEntityBuilder { _mangaFile = new MangaFile() { - FilePath = Parser.NormalizePath(filePath), + FilePath = filePath, Format = format, Pages = pages, LastModified = File.GetLastWriteTime(filePath), LastModifiedUtc = File.GetLastWriteTimeUtc(filePath), - FileName = Parser.RemoveExtensionIfSupported(filePath) }; } @@ -60,17 +58,4 @@ public class MangaFileBuilder : IEntityBuilder _mangaFile.Id = Math.Max(id, 0); return this; } - - /// - /// Generate the Hash on the underlying file - /// - /// Only applicable to Epubs - public MangaFileBuilder WithHash() - { - if (_mangaFile.Format != MangaFormat.Epub) return this; - - _mangaFile.KoreaderHash = KoreaderHelper.HashContents(_mangaFile.FilePath); - - return this; - } } diff --git a/API/Helpers/Builders/MediaErrorBuilder.cs b/API/Helpers/Builders/MediaErrorBuilder.cs index 4d0f7f3a0..56b19ba33 100644 --- a/API/Helpers/Builders/MediaErrorBuilder.cs +++ b/API/Helpers/Builders/MediaErrorBuilder.cs @@ -1,6 +1,5 @@ using System.IO; using API.Entities; -using API.Services.Tasks.Scanner.Parser; namespace API.Helpers.Builders; @@ -13,7 +12,7 @@ public class MediaErrorBuilder : IEntityBuilder { _mediaError = new MediaError() { - FilePath = Parser.NormalizePath(filePath), + FilePath = filePath, Extension = Path.GetExtension(filePath).Replace(".", string.Empty).ToUpperInvariant() }; } diff --git a/API/Helpers/Builders/PersonAliasBuilder.cs b/API/Helpers/Builders/PersonAliasBuilder.cs deleted file mode 100644 index e54ea8975..000000000 --- a/API/Helpers/Builders/PersonAliasBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -using API.Entities.Person; -using API.Extensions; - -namespace API.Helpers.Builders; - -public class PersonAliasBuilder : IEntityBuilder -{ - private readonly PersonAlias _alias; - public PersonAlias Build() => _alias; - - public PersonAliasBuilder(string name) - { - _alias = new PersonAlias() - { - Alias = name.Trim(), - NormalizedAlias = name.ToNormalized(), - }; - } -} diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs index afd0c84af..e7e1b573e 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using System.Linq; -using API.Entities.Person; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; using API.Extensions; namespace API.Helpers.Builders; @@ -10,14 +11,15 @@ public class PersonBuilder : IEntityBuilder private readonly Person _person; public Person Build() => _person; - public PersonBuilder(string name) + public PersonBuilder(string name, PersonRole role) { _person = new Person() { Name = name.Trim(), NormalizedName = name.ToNormalized(), - SeriesMetadataPeople = new List(), - ChapterPeople = new List() + Role = role, + ChapterMetadatas = new List(), + SeriesMetadatas = new List() }; } @@ -32,24 +34,10 @@ public class PersonBuilder : IEntityBuilder return this; } - public PersonBuilder WithAlias(string alias) + public PersonBuilder WithSeriesMetadata(SeriesMetadata metadata) { - if (_person.Aliases.Any(a => a.NormalizedAlias.Equals(alias.ToNormalized()))) - { - return this; - } - - _person.Aliases.Add(new PersonAliasBuilder(alias).Build()); - + _person.SeriesMetadatas ??= new List(); + _person.SeriesMetadatas.Add(metadata); return this; } - - - - public PersonBuilder WithSeriesMetadata(SeriesMetadataPeople seriesMetadataPeople) - { - _person.SeriesMetadataPeople.Add(seriesMetadataPeople); - return this; - } - } diff --git a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs index 3da217b9f..b379242ac 100644 --- a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs +++ b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs @@ -1,16 +1,14 @@ using System.Linq; using API.DTOs; -using API.DTOs.Scrobbling; using API.Entities; -using API.Extensions; using API.Services.Plus; namespace API.Helpers.Builders; -public class PlusSeriesDtoBuilder : IEntityBuilder +public class PlusSeriesDtoBuilder : IEntityBuilder { - private readonly PlusSeriesRequestDto _seriesRequestDto; - public PlusSeriesRequestDto Build() => _seriesRequestDto; + private readonly PlusSeriesDto _seriesDto; + public PlusSeriesDto Build() => _seriesDto; /// /// This must be a FULL Series @@ -18,9 +16,9 @@ public class PlusSeriesDtoBuilder : IEntityBuilder /// public PlusSeriesDtoBuilder(Series series) { - _seriesRequestDto = new PlusSeriesRequestDto() + _seriesDto = new PlusSeriesDto() { - MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), SeriesName = series.Name, AltSeriesName = series.LocalizedName, AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, @@ -29,8 +27,6 @@ public class PlusSeriesDtoBuilder : IEntityBuilder ScrobblingService.MalWeblinkWebsite), GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MangaDexWeblinkWebsite), VolumeCount = series.Volumes.Count, ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), Year = series.Metadata.ReleaseYear diff --git a/API/Helpers/Builders/RatingBuilder.cs b/API/Helpers/Builders/RatingBuilder.cs index 54af47ae8..4656c1fa1 100644 --- a/API/Helpers/Builders/RatingBuilder.cs +++ b/API/Helpers/Builders/RatingBuilder.cs @@ -28,7 +28,6 @@ public class RatingBuilder : IEntityBuilder public RatingBuilder WithTagline(string? tagline) { - if (string.IsNullOrEmpty(tagline)) return this; _rating.Tagline = tagline; return this; } diff --git a/API/Helpers/Builders/ScrobbleHoldBuilder.cs b/API/Helpers/Builders/ScrobbleHoldBuilder.cs index cd03a08f0..c09859f3c 100644 --- a/API/Helpers/Builders/ScrobbleHoldBuilder.cs +++ b/API/Helpers/Builders/ScrobbleHoldBuilder.cs @@ -1,7 +1,6 @@ using API.Entities.Scrobble; namespace API.Helpers.Builders; -#nullable enable public class ScrobbleHoldBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/SeriesBuilder.cs b/API/Helpers/Builders/SeriesBuilder.cs index 96e820659..5d73dcd6d 100644 --- a/API/Helpers/Builders/SeriesBuilder.cs +++ b/API/Helpers/Builders/SeriesBuilder.cs @@ -21,18 +21,13 @@ public class SeriesBuilder : IEntityBuilder _series = new Series() { Name = name, - LocalizedName = name.ToNormalized(), - NormalizedLocalizedName = name.ToNormalized(), - OriginalName = name, SortName = name, NormalizedName = name.ToNormalized(), - Metadata = new SeriesMetadataBuilder() - .WithPublicationStatus(PublicationStatus.OnGoing) - .Build(), - Volumes = new List(), - ExternalSeriesMetadata = new ExternalSeriesMetadata() + NormalizedLocalizedName = name.ToNormalized(), + Metadata = new SeriesMetadataBuilder().Build(), + Volumes = new List() }; } @@ -41,25 +36,14 @@ public class SeriesBuilder : IEntityBuilder /// /// /// - public SeriesBuilder WithLocalizedName(string localizedName, bool lockStatus = false) + public SeriesBuilder WithLocalizedName(string localizedName) { - // Why is this here? if (string.IsNullOrEmpty(localizedName)) { localizedName = _series.Name; } - _series.LocalizedName = localizedName; _series.NormalizedLocalizedName = localizedName.ToNormalized(); - _series.LocalizedNameLocked = lockStatus; - return this; - } - - public SeriesBuilder WithLocalizedNameAllowEmpty(string localizedName, bool lockStatus = false) - { - _series.LocalizedName = localizedName; - _series.NormalizedLocalizedName = localizedName.ToNormalized(); - _series.LocalizedNameLocked = lockStatus; return this; } @@ -105,29 +89,4 @@ public class SeriesBuilder : IEntityBuilder _series.LibraryId = id; return this; } - - public SeriesBuilder WithPublicationStatus(PublicationStatus status) - { - _series.Metadata.PublicationStatus = status; - return this; - } - - public SeriesBuilder WithExternalMetadata(ExternalSeriesMetadata metadata) - { - _series.ExternalSeriesMetadata = metadata; - return this; - } - - - public SeriesBuilder WithRelationship(int targetSeriesId, RelationKind kind) - { - _series.Relations ??= []; - _series.Relations.Add(new SeriesRelation() - { - RelationKind = kind, - TargetSeriesId = targetSeriesId - }); - - return this; - } } diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs index 462bc4455..d90e896ef 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; -using API.Entities.Person; namespace API.Helpers.Builders; @@ -19,19 +17,16 @@ public class SeriesMetadataBuilder : IEntityBuilder CollectionTags = new List(), Genres = new List(), Tags = new List(), - People = new List() + People = new List() }; } - [Obsolete] public SeriesMetadataBuilder WithCollectionTag(CollectionTag tag) { _seriesMetadata.CollectionTags ??= new List(); _seriesMetadata.CollectionTags.Add(tag); return this; } - - [Obsolete] public SeriesMetadataBuilder WithCollectionTags(IList tags) { if (tags == null) return this; @@ -39,92 +34,15 @@ public class SeriesMetadataBuilder : IEntityBuilder _seriesMetadata.CollectionTags = tags; return this; } - - public SeriesMetadataBuilder WithPublicationStatus(PublicationStatus status, bool lockState = false) + public SeriesMetadataBuilder WithPublicationStatus(PublicationStatus status) { _seriesMetadata.PublicationStatus = status; - _seriesMetadata.PublicationStatusLocked = lockState; return this; } - public SeriesMetadataBuilder WithAgeRating(AgeRating rating, bool lockState = false) + public SeriesMetadataBuilder WithAgeRating(AgeRating rating) { _seriesMetadata.AgeRating = rating; - _seriesMetadata.AgeRatingLocked = lockState; - return this; - } - - public SeriesMetadataBuilder WithPerson(Person person, PersonRole role) - { - _seriesMetadata.People ??= new List(); - _seriesMetadata.People.Add(new SeriesMetadataPeople() - { - Role = role, - Person = person, - SeriesMetadata = _seriesMetadata, - }); - return this; - } - - public SeriesMetadataBuilder WithLanguage(string languageCode) - { - _seriesMetadata.Language = languageCode; - return this; - } - - public SeriesMetadataBuilder WithReleaseYear(int year, bool lockStatus = false) - { - _seriesMetadata.ReleaseYear = year; - _seriesMetadata.ReleaseYearLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithSummary(string summary, bool lockStatus = false) - { - _seriesMetadata.Summary = summary; - _seriesMetadata.SummaryLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithGenre(Genre genre, bool lockStatus = false) - { - _seriesMetadata.Genres ??= []; - _seriesMetadata.Genres.Add(genre); - _seriesMetadata.GenresLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithGenres(List genres, bool lockStatus = false) - { - _seriesMetadata.Genres = genres; - _seriesMetadata.GenresLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithTag(Tag tag, bool lockStatus = false) - { - _seriesMetadata.Tags ??= []; - _seriesMetadata.Tags.Add(tag); - _seriesMetadata.TagsLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithTags(List tags, bool lockStatus = false) - { - _seriesMetadata.Tags = tags; - _seriesMetadata.TagsLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithMaxCount(int count) - { - _seriesMetadata.MaxCount = count; - return this; - } - - public SeriesMetadataBuilder WithTotalCount(int count) - { - _seriesMetadata.TotalCount = count; return this; } } diff --git a/API/Helpers/Builders/SmartFilterBuilder.cs b/API/Helpers/Builders/SmartFilterBuilder.cs new file mode 100644 index 000000000..538d8a529 --- /dev/null +++ b/API/Helpers/Builders/SmartFilterBuilder.cs @@ -0,0 +1,24 @@ +using API.DTOs.Filtering.v2; +using API.Entities; + +namespace API.Helpers.Builders; + +public class SmartFilterBuilder : IEntityBuilder +{ + private AppUserSmartFilter _smartFilter; + public AppUserSmartFilter Build() => _smartFilter; + + public SmartFilterBuilder(FilterV2Dto filter) + { + _smartFilter = new AppUserSmartFilter() + { + Name = filter.Name, + Filter = SmartFilterHelper.Encode(filter) + }; + } + + // public SmartFilterBuilder WithName(string name) + // { + // + // } +} diff --git a/API/Helpers/Builders/TagBuilder.cs b/API/Helpers/Builders/TagBuilder.cs index 623587fd1..084171f54 100644 --- a/API/Helpers/Builders/TagBuilder.cs +++ b/API/Helpers/Builders/TagBuilder.cs @@ -16,8 +16,8 @@ public class TagBuilder : IEntityBuilder { Title = name.Trim().SentenceCase(), NormalizedTitle = name.ToNormalized(), - Chapters = [], - SeriesMetadatas = [] + Chapters = new List(), + SeriesMetadatas = new List() }; } diff --git a/API/Helpers/Builders/VolumeBuilder.cs b/API/Helpers/Builders/VolumeBuilder.cs index 8d98844aa..057c3bd99 100644 --- a/API/Helpers/Builders/VolumeBuilder.cs +++ b/API/Helpers/Builders/VolumeBuilder.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using API.Data; using API.Entities; @@ -16,9 +15,7 @@ public class VolumeBuilder : IEntityBuilder _volume = new Volume() { Name = volumeNumber, - LookupName = volumeNumber, - MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), - MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), + Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), Chapters = new List() }; } @@ -29,29 +26,13 @@ public class VolumeBuilder : IEntityBuilder return this; } - public VolumeBuilder WithNumber(float number) + public VolumeBuilder WithNumber(int number) { - _volume.MinNumber = number; - if (_volume.MaxNumber < number) - { - _volume.MaxNumber = number; - } + _volume.Number = number; return this; } - public VolumeBuilder WithMinNumber(float number) - { - _volume.MinNumber = number; - return this; - } - - public VolumeBuilder WithMaxNumber(float number) - { - _volume.MaxNumber = number; - return this; - } - - public VolumeBuilder WithChapters(IList chapters) + public VolumeBuilder WithChapters(List chapters) { _volume.Chapters = chapters; return this; @@ -76,18 +57,4 @@ public class VolumeBuilder : IEntityBuilder _volume.CoverImage = cover; return this; } - - public VolumeBuilder WithCreated(DateTime created) - { - _volume.Created = created; - _volume.CreatedUtc = created.ToUniversalTime(); - return this; - } - - public VolumeBuilder WithLastModified(DateTime lastModified) - { - _volume.LastModified = lastModified; - _volume.LastModifiedUtc = lastModified.ToUniversalTime(); - return this; - } } diff --git a/API/Helpers/CacheHelper.cs b/API/Helpers/CacheHelper.cs index ede5caaef..d7e22c2e6 100644 --- a/API/Helpers/CacheHelper.cs +++ b/API/Helpers/CacheHelper.cs @@ -4,7 +4,6 @@ using API.Entities.Interfaces; using API.Services; namespace API.Helpers; -#nullable enable public interface ICacheHelper { @@ -14,8 +13,8 @@ public interface ICacheHelper bool CoverImageExists(string path); - bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile); - bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile); + bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile); + bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile); } @@ -56,7 +55,7 @@ public class CacheHelper : ICacheHelper /// /// /// - public bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile) + public bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile) { return firstFile != null && (!forceUpdate && @@ -71,7 +70,7 @@ public class CacheHelper : ICacheHelper /// Should we ignore any logic and force this to return true /// The file in question /// - public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile) + public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile) { if (firstFile == null) return false; if (forceUpdate) return true; diff --git a/API/Helpers/Converters/CronConverter.cs b/API/Helpers/Converters/CronConverter.cs index f1f0ebc1b..4e9547c6c 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/API/Helpers/Converters/CronConverter.cs @@ -2,7 +2,6 @@ using Hangfire; namespace API.Helpers.Converters; -#nullable enable public static class CronConverter { @@ -12,21 +11,18 @@ public static class CronConverter "daily", "weekly", }; - /// - /// Converts to Cron Notation - /// - /// Defaults to daily - /// - public static string ConvertToCronNotation(string? source) + public static string ConvertToCronNotation(string source) { - if (string.IsNullOrEmpty(source)) return Cron.Daily(); - return source.ToLower() switch + var destination = string.Empty; + destination = source.ToLower() switch { "daily" => Cron.Daily(), "weekly" => Cron.Weekly(), "disabled" => Cron.Never(), "" => Cron.Never(), - _ => source + _ => destination }; + + return destination; } } diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index 631332f5f..7f4001f67 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -7,7 +7,6 @@ using API.Entities.Enums; using API.Extensions; namespace API.Helpers.Converters; -#nullable enable public static class FilterFieldValueConverter { @@ -18,95 +17,65 @@ public static class FilterFieldValueConverter FilterField.SeriesName => value, FilterField.Path => value, FilterField.FilePath => value, - FilterField.ReleaseYear => string.IsNullOrEmpty(value) ? 0 : int.Parse(value), + FilterField.ReleaseYear => int.Parse(value), FilterField.Languages => value.Split(',').ToList(), FilterField.PublicationStatus => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(x => (PublicationStatus) Enum.Parse(typeof(PublicationStatus), x)) .ToList(), FilterField.Summary => value, FilterField.AgeRating => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(x => (AgeRating) Enum.Parse(typeof(AgeRating), x)) .ToList(), - FilterField.UserRating => string.IsNullOrEmpty(value) ? 0 : float.Parse(value), + FilterField.UserRating => int.Parse(value), FilterField.Tags => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.CollectionTags => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Translators => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Characters => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Publisher => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Editor => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.CoverArtist => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Letterer => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Colorist => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Inker => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) - .Select(int.Parse) - .ToList(), - FilterField.Imprint => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) - .Select(int.Parse) - .ToList(), - FilterField.Team => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) - .Select(int.Parse) - .ToList(), - FilterField.Location => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Penciller => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Writers => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Genres => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.Libraries => value.Split(',') - .Where(s => !string.IsNullOrEmpty(s)) .Select(int.Parse) .ToList(), FilterField.WantToRead => bool.Parse(value), - FilterField.ReadProgress => string.IsNullOrEmpty(value) ? 0f : value.AsFloat(), - FilterField.ReadingDate => DateTime.Parse(value, CultureInfo.InvariantCulture), - FilterField.ReadLast => int.Parse(value), + FilterField.ReadProgress => value.AsFloat(), + FilterField.ReadingDate => DateTime.Parse(value), FilterField.Formats => value.Split(',') .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) .ToList(), - FilterField.ReadTime => string.IsNullOrEmpty(value) ? 0 : int.Parse(value), - FilterField.AverageRating => string.IsNullOrEmpty(value) ? 0f : value.AsFloat(), + FilterField.ReadTime => int.Parse(value), _ => throw new ArgumentException("Invalid field type") }; } diff --git a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs b/API/Helpers/Converters/PersonFilterFieldValueConverter.cs deleted file mode 100644 index 822ce105a..000000000 --- a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; - -namespace API.Helpers.Converters; - -public static class PersonFilterFieldValueConverter -{ - public static object ConvertValue(PersonFilterField field, string value) - { - return field switch - { - PersonFilterField.Name => value, - PersonFilterField.Role => ParsePersonRoles(value), - PersonFilterField.SeriesCount => int.Parse(value), - PersonFilterField.ChapterCount => int.Parse(value), - _ => throw new ArgumentOutOfRangeException(nameof(field), field, "Field is not supported") - }; - } - - private static IList ParsePersonRoles(string value) - { - if (string.IsNullOrEmpty(value)) return []; - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(v => Enum.Parse(v.Trim())) - .ToList(); - } -} diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 7adb5228f..a55e104a7 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using AutoMapper; namespace API.Helpers.Converters; -#nullable enable public class ServerSettingConverter : ITypeConverter, ServerSettingDto> { @@ -24,17 +22,14 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.TaskScan: destination.TaskScan = row.Value; break; - case ServerSettingKey.TaskBackup: - destination.TaskBackup = row.Value; - break; - case ServerSettingKey.TaskCleanup: - destination.TaskCleanup = row.Value; - break; case ServerSettingKey.LoggingLevel: destination.LoggingLevel = row.Value; break; + case ServerSettingKey.TaskBackup: + destination.TaskBackup = row.Value; + break; case ServerSettingKey.Port: - destination.Port = int.Parse(row.Value, CultureInfo.InvariantCulture); + destination.Port = int.Parse(row.Value); break; case ServerSettingKey.IpAddresses: destination.IpAddresses = row.Value; @@ -51,11 +46,17 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.BookmarkDirectory: destination.BookmarksDirectory = row.Value; break; + case ServerSettingKey.EmailServiceUrl: + destination.EmailServiceUrl = row.Value; + break; case ServerSettingKey.InstallVersion: destination.InstallVersion = row.Value; break; + case ServerSettingKey.EncodeMediaAs: + destination.EncodeMediaAs = Enum.Parse(row.Value); + break; case ServerSettingKey.TotalBackups: - destination.TotalBackups = int.Parse(row.Value, CultureInfo.InvariantCulture); + destination.TotalBackups = int.Parse(row.Value); break; case ServerSettingKey.InstallId: destination.InstallId = row.Value; @@ -64,78 +65,23 @@ public class ServerSettingConverter : ITypeConverter, destination.EnableFolderWatching = bool.Parse(row.Value); break; case ServerSettingKey.TotalLogs: - destination.TotalLogs = int.Parse(row.Value, CultureInfo.InvariantCulture); + destination.TotalLogs = int.Parse(row.Value); break; case ServerSettingKey.HostName: destination.HostName = row.Value; break; case ServerSettingKey.CacheSize: - destination.CacheSize = long.Parse(row.Value, CultureInfo.InvariantCulture); + destination.CacheSize = long.Parse(row.Value); break; case ServerSettingKey.OnDeckProgressDays: - destination.OnDeckProgressDays = int.Parse(row.Value, CultureInfo.InvariantCulture); + destination.OnDeckProgressDays = int.Parse(row.Value); break; case ServerSettingKey.OnDeckUpdateDays: - destination.OnDeckUpdateDays = int.Parse(row.Value, CultureInfo.InvariantCulture); + destination.OnDeckUpdateDays = int.Parse(row.Value); break; case ServerSettingKey.CoverImageSize: destination.CoverImageSize = Enum.Parse(row.Value); break; - case ServerSettingKey.EncodeMediaAs: - destination.EncodeMediaAs = Enum.Parse(row.Value); - break; - case ServerSettingKey.BackupDirectory: - destination.BookmarksDirectory = row.Value; - break; - case ServerSettingKey.EmailHost: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.Host = row.Value ?? string.Empty; - break; - case ServerSettingKey.EmailPort: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.Port = string.IsNullOrEmpty(row.Value) ? 0 : int.Parse(row.Value, CultureInfo.InvariantCulture); - break; - case ServerSettingKey.EmailAuthPassword: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.Password = row.Value; - break; - case ServerSettingKey.EmailAuthUserName: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.UserName = row.Value; - break; - case ServerSettingKey.EmailSenderAddress: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.SenderAddress = row.Value; - break; - case ServerSettingKey.EmailSenderDisplayName: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.SenderDisplayName = row.Value; - break; - case ServerSettingKey.EmailEnableSsl: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.EnableSsl = bool.Parse(row.Value); - break; - case ServerSettingKey.EmailSizeLimit: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.SizeLimit = int.Parse(row.Value, CultureInfo.InvariantCulture); - break; - case ServerSettingKey.EmailCustomizedTemplates: - destination.SmtpConfig ??= new SmtpConfigDto(); - destination.SmtpConfig.CustomizedTemplates = bool.Parse(row.Value); - break; - case ServerSettingKey.FirstInstallDate: - destination.FirstInstallDate = DateTime.Parse(row.Value, CultureInfo.InvariantCulture); - break; - case ServerSettingKey.FirstInstallVersion: - destination.FirstInstallVersion = row.Value; - break; - case ServerSettingKey.LicenseKey: - case ServerSettingKey.EnableAuthentication: - case ServerSettingKey.EmailServiceUrl: - case ServerSettingKey.ConvertBookmarkToWebP: - case ServerSettingKey.ConvertCoverToWebP: - default: - break; } } diff --git a/API/Helpers/DayOfWeekHelper.cs b/API/Helpers/DayOfWeekHelper.cs deleted file mode 100644 index 10cdb4170..000000000 --- a/API/Helpers/DayOfWeekHelper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace API.Helpers; - -public static class DayOfWeekHelper -{ - private static readonly Random Rnd = new(); - - /// - /// Returns a random DayOfWeek value. - /// - /// A randomly selected DayOfWeek. - public static DayOfWeek Random() - { - var values = Enum.GetValues(); - return (DayOfWeek)values.GetValue(Rnd.Next(values.Length))!; - } -} diff --git a/API/Helpers/GenreHelper.cs b/API/Helpers/GenreHelper.cs index 8580178d9..721981054 100644 --- a/API/Helpers/GenreHelper.cs +++ b/API/Helpers/GenreHelper.cs @@ -1,131 +1,104 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using API.Data; using API.DTOs.Metadata; using API.Entities; using API.Extensions; using API.Helpers.Builders; -using Microsoft.EntityFrameworkCore; namespace API.Helpers; #nullable enable - public static class GenreHelper { - - public static async Task UpdateChapterGenres(Chapter chapter, IEnumerable genreNames, IUnitOfWork unitOfWork) + public static void UpdateGenre(ICollection allGenres, IEnumerable names, Action action) { - // Normalize genre names once and store them in a hash set for quick lookups - var normalizedToOriginal = genreNames - .Select(g => new { Original = g, Normalized = g.ToNormalized() }) - .GroupBy(x => x.Normalized) - .ToDictionary(g => g.Key, g => g.First().Original); - - var normalizedGenresToAdd = new HashSet(normalizedToOriginal.Keys); - - // Remove genres that are no longer in the new list - var genresToRemove = chapter.Genres - .Where(g => !normalizedGenresToAdd.Contains(g.NormalizedTitle)) - .ToList(); - - if (genresToRemove.Count > 0) + foreach (var name in names) { - foreach (var genreToRemove in genresToRemove) + if (string.IsNullOrEmpty(name.Trim())) continue; + + var normalizedName = name.ToNormalized(); + var genre = allGenres.FirstOrDefault(p => p.NormalizedTitle != null && p.NormalizedTitle.Equals(normalizedName)); + if (genre == null) { - chapter.Genres.Remove(genreToRemove); + genre = new GenreBuilder(name).Build(); + allGenres.Add(genre); } - } - // Get all normalized titles to query the database for existing genres - var existingGenreTitles = await unitOfWork.DataContext.Genre - .Where(g => normalizedGenresToAdd.Contains(g.NormalizedTitle)) - .ToDictionaryAsync(g => g.NormalizedTitle); - - // Find missing genres that are not in the database - var missingGenres = normalizedGenresToAdd - .Where(nt => !existingGenreTitles.ContainsKey(nt)) - .Select(nt => new GenreBuilder(normalizedToOriginal[nt]).Build()) - .ToList(); - - // Add missing genres to the database - if (missingGenres.Count > 0) - { - unitOfWork.DataContext.Genre.AddRange(missingGenres); - await unitOfWork.CommitAsync(); - - // Add newly inserted genres to existing genres dictionary for easier lookup - foreach (var genre in missingGenres) - { - existingGenreTitles[genre.NormalizedTitle] = genre; - } - } - - // Add genres that are either existing or newly added to the chapter - foreach (var normalizedTitle in normalizedGenresToAdd) - { - var genre = existingGenreTitles[normalizedTitle]; - - if (!chapter.Genres.Contains(genre)) - { - chapter.Genres.Add(genre); - } + action(genre); } } - public static void UpdateGenreList(ICollection? existingGenres, Series series, - IReadOnlyCollection newGenres, Action handleAdd, Action onModified) + public static void KeepOnlySameGenreBetweenLists(ICollection existingGenres, ICollection removeAllExcept, Action? action = null) { - UpdateGenreList(existingGenres.DefaultIfEmpty().Select(t => t.Title).ToList(), series, newGenres, handleAdd, onModified); + var existing = existingGenres.ToList(); + foreach (var genre in existing) + { + var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle != null && genre.NormalizedTitle.Equals(g.NormalizedTitle)); + if (existingPerson != null) continue; + existingGenres.Remove(genre); + action?.Invoke(genre); + } + } - public static void UpdateGenreList(ICollection? existingGenres, Series series, - IReadOnlyCollection newGenres, Action handleAdd, Action onModified) + /// + /// Adds the genre to the list if it's not already in there. + /// + /// + /// + public static void AddGenreIfNotExists(ICollection metadataGenres, Genre genre) { - if (existingGenres == null) return; + var existingGenre = metadataGenres.FirstOrDefault(p => + p.NormalizedTitle.Equals(genre.Title?.ToNormalized())); + if (existingGenre == null) + { + metadataGenres.Add(genre); + } + } + + + public static void UpdateGenreList(ICollection? tags, Series series, + IReadOnlyCollection allTags, Action handleAdd, Action onModified) + { + if (tags == null) return; var isModified = false; - - // Convert tags and existing genres to hash sets for quick lookups by normalized title - var tagSet = new HashSet(existingGenres.Select(t => t.ToNormalized())); - var genreSet = new HashSet(series.Metadata.Genres.Select(g => g.NormalizedTitle)); - - // Remove tags that are no longer present in the input tags - var existingTags = series.Metadata.Genres.ToList(); // Copy to avoid modifying collection while iterating + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.Genres.ToList(); foreach (var existing in existingTags) { - if (!tagSet.Contains(existing.NormalizedTitle)) // This correctly ensures removal of non-present tags + if (tags.SingleOrDefault(t => t.Title.ToNormalized().Equals(existing.NormalizedTitle)) == null) { + // Remove tag series.Metadata.Genres.Remove(existing); isModified = true; } } - // Prepare a dictionary for quick lookup of genres from the `newGenres` collection by normalized title - var allTagsDict = newGenres.ToDictionary(t => t.NormalizedTitle); - - // Add new tags from the input list - foreach (var tagDto in existingGenres) + // At this point, all tags that aren't in dto have been removed. + foreach (var tagTitle in tags.Select(t => t.Title)) { - var normalizedTitle = tagDto.ToNormalized(); - - if (genreSet.Contains(normalizedTitle)) continue; // This prevents re-adding existing genres - - if (allTagsDict.TryGetValue(normalizedTitle, out var existingTag)) + var normalizedTitle = tagTitle.ToNormalized(); + var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle)); + if (existingTag != null) { - handleAdd(existingTag); // Add existing tag from allTagsDict + if (series.Metadata.Genres.All(t => !t.NormalizedTitle.Equals(normalizedTitle))) + { + handleAdd(existingTag); + isModified = true; + } } else { - handleAdd(new GenreBuilder(tagDto).Build()); // Add new genre if not found + // Add new tag + handleAdd(new GenreBuilder(tagTitle).Build()); + isModified = true; } - isModified = true; } - // Call onModified if any changes were made if (isModified) { onModified(); diff --git a/API/Helpers/JwtHelper.cs b/API/Helpers/JwtHelper.cs deleted file mode 100644 index 0f9219804..000000000 --- a/API/Helpers/JwtHelper.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; - -namespace API.Helpers; - -public static class JwtHelper -{ - /// - /// Extracts the expiration date from a JWT token. - /// - public static DateTime GetTokenExpiry(string jwtToken) - { - if (string.IsNullOrEmpty(jwtToken)) - return DateTime.MinValue; - - // Parse the JWT and extract the expiry claim - var jwtHandler = new JwtSecurityTokenHandler(); - var token = jwtHandler.ReadJwtToken(jwtToken); - return token.ValidTo; - - // var exp = token.Claims.FirstOrDefault(c => c.Type == "exp")?.Value; - // - // if (long.TryParse(exp, CultureInfo.InvariantCulture, out var expSeconds)) - // { - // return DateTimeOffset.FromUnixTimeSeconds(expSeconds).UtcDateTime; - // } - // - // - // - // return DateTime.MinValue; - } - - /// - /// Checks if a JWT token is valid based on its expiry date. - /// - public static bool IsTokenValid(string jwtToken) - { - if (string.IsNullOrEmpty(jwtToken)) return false; - - var expiry = GetTokenExpiry(jwtToken); - return expiry > DateTime.UtcNow; - } -} diff --git a/API/Helpers/KoreaderHelper.cs b/API/Helpers/KoreaderHelper.cs deleted file mode 100644 index e779cd911..000000000 --- a/API/Helpers/KoreaderHelper.cs +++ /dev/null @@ -1,113 +0,0 @@ -using API.DTOs.Progress; -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers; - -/// -/// All things related to Koreader -/// -/// Original developer: https://github.com/MFDeAngelo -public static class KoreaderHelper -{ - /// - /// Hashes the document according to a custom Koreader hashing algorithm. - /// Look at the util.partialMD5 method in the attached link. - /// Note: Only applies to epub files - /// - /// The hashing algorithm is relatively quick as it only hashes ~10,000 bytes for the biggest of files. - /// - /// The path to the file to hash - public static string HashContents(string filePath) - { - if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath) || !Parser.IsEpub(filePath)) - { - return null; - } - - using var file = File.OpenRead(filePath); - - const int step = 1024; - const int size = 1024; - var md5 = MD5.Create(); - var buffer = new byte[size]; - - for (var i = -1; i < 10; i++) - { - file.Position = step << 2 * i; - var bytesRead = file.Read(buffer, 0, size); - if (bytesRead > 0) - { - md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); - } - else - { - break; - } - } - - file.Close(); - md5.TransformFinalBlock([], 0, 0); - - return md5.Hash == null ? null : Convert.ToHexString(md5.Hash).ToUpper(); - } - - /// - /// Koreader can identify documents based on contents or title. - /// For now, we only support by contents. - /// - public static string HashTitle(string filePath) - { - var fileName = Path.GetFileName(filePath); - var fileNameBytes = Encoding.ASCII.GetBytes(fileName); - var bytes = MD5.HashData(fileNameBytes); - - return Convert.ToHexString(bytes); - } - - public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition) - { - var path = koreaderPosition.Split('/'); - if (path.Length < 6) - { - return; - } - - var docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty); - progress.PageNum = int.Parse(docNumber) - 1; - var lastTag = path[5].ToUpper(); - - if (lastTag == "A") - { - progress.BookScrollId = null; - } - else - { - // The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off. - progress.BookScrollId = $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/{lastTag}"; - } - } - - - public static string GetKoreaderPosition(ProgressDto progressDto) - { - string lastTag; - var koreaderPageNumber = progressDto.PageNum + 1; - - if (string.IsNullOrEmpty(progressDto.BookScrollId)) - { - lastTag = "a"; - } - else - { - var tokens = progressDto.BookScrollId.Split('/'); - lastTag = tokens[^1].ToLower(); - } - - // The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off. - return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}"; - } -} diff --git a/API/Helpers/LibraryTypeHelper.cs b/API/Helpers/LibraryTypeHelper.cs new file mode 100644 index 000000000..f2d320621 --- /dev/null +++ b/API/Helpers/LibraryTypeHelper.cs @@ -0,0 +1,19 @@ +using System; +using API.DTOs.Scrobbling; +using API.Entities.Enums; + +namespace API.Helpers; + +public static class LibraryTypeHelper +{ + public static MediaFormat GetFormat(LibraryType libraryType) + { + return libraryType switch + { + LibraryType.Manga => MediaFormat.Manga, + LibraryType.Comic => MediaFormat.Comic, + LibraryType.Book => MediaFormat.LightNovel, + _ => throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null) + }; + } +} diff --git a/API/Helpers/NumberHelper.cs b/API/Helpers/NumberHelper.cs index 906e405cc..b15f7e680 100644 --- a/API/Helpers/NumberHelper.cs +++ b/API/Helpers/NumberHelper.cs @@ -1,5 +1,4 @@ namespace API.Helpers; -#nullable enable public static class NumberHelper { diff --git a/API/Helpers/OrderableHelper.cs b/API/Helpers/OrderableHelper.cs index d4ff89573..06a53f575 100644 --- a/API/Helpers/OrderableHelper.cs +++ b/API/Helpers/OrderableHelper.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using API.Entities; namespace API.Helpers; -#nullable enable public static class OrderableHelper { public static void ReorderItems(List items, int itemId, int toPosition) { - if (toPosition < 0) throw new ArgumentException("toPosition cannot be less than 0"); - var item = items.Find(r => r.Id == itemId); if (item != null) { @@ -26,8 +22,6 @@ public static class OrderableHelper public static void ReorderItems(List items, int itemId, int toPosition) { - if (toPosition < 0) throw new ArgumentException("toPosition cannot be less than 0"); - var item = items.Find(r => r.Id == itemId); if (item != null && toPosition < items.Count) { @@ -51,16 +45,10 @@ public static class OrderableHelper public static void ReorderItems(List items, int readingListItemId, int toPosition) { - if (toPosition < 0) throw new ArgumentException("toPosition cannot be less than 0"); - var item = items.Find(r => r.Id == readingListItemId); if (item != null) { items.Remove(item); - - // Ensure toPosition is within the new list bounds - toPosition = Math.Min(toPosition, items.Count); - items.Insert(toPosition, item); } diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs index 44d8a5082..0c666612d 100644 --- a/API/Helpers/PagedList.cs +++ b/API/Helpers/PagedList.cs @@ -5,11 +5,10 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace API.Helpers; -#nullable enable public class PagedList : List { - private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) + public PagedList(IEnumerable items, int count, int pageNumber, int pageSize) { CurrentPage = pageNumber; TotalPages = (int) Math.Ceiling(count / (double) pageSize); diff --git a/API/Helpers/PaginationHeader.cs b/API/Helpers/PaginationHeader.cs index b11c5ecd4..d3c582798 100644 --- a/API/Helpers/PaginationHeader.cs +++ b/API/Helpers/PaginationHeader.cs @@ -1,5 +1,4 @@ namespace API.Helpers; -#nullable enable public class PaginationHeader { diff --git a/API/Helpers/ParserInfoHelpers.cs b/API/Helpers/ParserInfoHelpers.cs index fc8d7227a..dbd2f57da 100644 --- a/API/Helpers/ParserInfoHelpers.cs +++ b/API/Helpers/ParserInfoHelpers.cs @@ -6,7 +6,6 @@ using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; namespace API.Helpers; -#nullable enable public static class ParserInfoHelpers { @@ -40,7 +39,7 @@ public static class ParserInfoHelpers } } - if (series.Format == MangaFormat.Unknown) + if (series.Format == MangaFormat.Unknown && format != MangaFormat.Unknown) { return true; } diff --git a/API/Helpers/PdfComicInfoExtractor.cs b/API/Helpers/PdfComicInfoExtractor.cs deleted file mode 100644 index ce74ae97d..000000000 --- a/API/Helpers/PdfComicInfoExtractor.cs +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Contributed by https://github.com/microtherion - * - * All references to the "PDF Spec" (section numbers, etc) refer to the - * PDF 1.7 Specification a.k.a. PDF32000-1:2008 - * https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf - */ -using System; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using Nager.ArticleNumber; -using System.Collections.Generic; -using System.Globalization; - -namespace API.Helpers; -#nullable enable - -public interface IPdfComicInfoExtractor -{ - ComicInfo? GetComicInfo(string filePath); -} - -/// -/// Translate PDF metadata (See PdfMetadataExtractor.cs) into ComicInfo structure. -/// -public class PdfComicInfoExtractor : IPdfComicInfoExtractor -{ - private readonly ILogger _logger; - private readonly IMediaErrorService _mediaErrorService; - private readonly string[] _pdfDateFormats = [ // PDF Spec 7.9.4 - "D:yyyyMMddHHmmsszzz:", "D:yyyyMMddHHmmss+", "D:yyyyMMddHHmmss", - "D:yyyyMMddHHmmzzz:", "D:yyyyMMddHHmm+", "D:yyyyMMddHHmm", - "D:yyyyMMddHHzzz:", "D:yyyyMMddHH+", "D:yyyyMMddHH", - "D:yyyyMMdd", "D:yyyyMM", "D:yyyy" - ]; - - public PdfComicInfoExtractor(ILogger logger, IMediaErrorService mediaErrorService) - { - _logger = logger; - _mediaErrorService = mediaErrorService; - } - - private static float? GetFloatFromText(string? text) - { - if (string.IsNullOrEmpty(text)) return null; - - if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) return value; - - return null; - } - - private DateTime? GetDateTimeFromText(string? text) - { - if (string.IsNullOrEmpty(text)) return null; - - // Dates stored in the XMP metadata stream (PDF Spec 14.3.2) - // are stored in ISO 8601 format, which is handled by C# out of the box - if (DateTime.TryParse(text, CultureInfo.InvariantCulture, out var date)) return date; - - // Dates stored in the document information directory (PDF Spec 14.3.3) - // are stored in a proprietary format (PDF Spec 7.9.4) that needs to be - // massaged slightly to be expressible by a DateTime format. - if (text[0] != 'D') { - text = "D:" + text; - } - text = text.Replace("'", ":"); - text = text.Replace("Z", "+"); - - foreach(var format in _pdfDateFormats) - { - if (DateTime.TryParseExact(text, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var pdfDate)) return pdfDate; - } - - return null; - } - - private static string? MaybeGetMetadata(Dictionary metadata, string key) - { - return metadata.TryGetValue(key, out var value) ? value : null; - } - - private ComicInfo? GetComicInfoFromMetadata(Dictionary metadata, string filePath) - { - var info = new ComicInfo(); - - var publicationDate = GetDateTimeFromText(MaybeGetMetadata(metadata, "CreationDate")); - - if (publicationDate != null) - { - info.Year = publicationDate.Value.Year; - info.Month = publicationDate.Value.Month; - info.Day = publicationDate.Value.Day; - } - - info.Summary = MaybeGetMetadata(metadata, "Summary") ?? string.Empty; - info.Publisher = MaybeGetMetadata(metadata, "Publisher") ?? string.Empty; - info.Writer = MaybeGetMetadata(metadata, "Author") ?? string.Empty; - info.Title = MaybeGetMetadata(metadata, "Title") ?? string.Empty; - info.TitleSort = MaybeGetMetadata(metadata, "TitleSort") ?? string.Empty; - info.Genre = MaybeGetMetadata(metadata, "Subject") ?? string.Empty; - info.LanguageISO = BookService.ValidateLanguage(MaybeGetMetadata(metadata, "Language")); - info.Isbn = MaybeGetMetadata(metadata, "ISBN") ?? string.Empty; - - if (info.Isbn != string.Empty && !ArticleNumberHelper.IsValidIsbn10(info.Isbn) && !ArticleNumberHelper.IsValidIsbn13(info.Isbn)) - { - _logger.LogDebug("[BookService] {File} has an invalid ISBN number", filePath); - info.Isbn = string.Empty; - } - - info.UserRating = GetFloatFromText(MaybeGetMetadata(metadata, "UserRating")) ?? 0.0f; - info.Series = MaybeGetMetadata(metadata, "Series") ?? info.Title; - info.SeriesSort = info.Series; - info.Volume = MaybeGetMetadata(metadata, "Volume") ?? string.Empty; - - // If this is a single book and not a collection, set publication status to Completed - if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath, LibraryType.Manga).Equals(Parser.LooseLeafVolume)) - { - info.Count = 1; - } - - ComicInfo.CleanComicInfo(info); - - return info; - } - - public ComicInfo? GetComicInfo(string filePath) - { - try - { - var extractor = new PdfMetadataExtractor(_logger, filePath); - - return GetComicInfoFromMetadata(extractor.GetMetadata(), filePath); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, - "There was an exception parsing PDF metadata", ex); - } - - return null; - } -} diff --git a/API/Helpers/PdfMetadataExtractor.cs b/API/Helpers/PdfMetadataExtractor.cs deleted file mode 100644 index 44327672b..000000000 --- a/API/Helpers/PdfMetadataExtractor.cs +++ /dev/null @@ -1,1637 +0,0 @@ -/** - * Contributed by https://github.com/microtherion - * - * All references to the "PDF Spec" (section numbers, etc) refer to the - * PDF 1.7 Specification a.k.a. PDF32000-1:2008 - * https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf - */ - -using System; -using System.Collections.Generic; -using System.IO.Compression; -using System.Text; -using System.Xml; -using System.IO; -using Microsoft.Extensions.Logging; -using API.Services; - -namespace API.Helpers; -#nullable enable - -/// -/// Parse PDF file and try to extract as much metadata as possible. -/// Supports both text based XRef tables and compressed XRef streams (Deflate only). -/// Supports both UTF-16 and PDFDocEncoding for strings. -/// Lacks support for many PDF configurations that are theoretically possible, but should handle most common cases. -/// -public class PdfMetadataExtractorException : Exception -{ - public PdfMetadataExtractorException() - { - } - - public PdfMetadataExtractorException(string message) - : base(message) - { - } - - public PdfMetadataExtractorException(string message, Exception inner) - : base(message, inner) - { - } -} - -public interface IPdfMetadataExtractor -{ - Dictionary GetMetadata(); -} - -class PdfStringBuilder -{ - private readonly StringBuilder _builder = new(); - private bool _secondByte = false; - private byte _prevByte = 0; - private bool _isUnicode = false; - - // PDFDocEncoding defined in PDF Spec D.1 - - private readonly char[] _pdfDocMappingLow = - [ - '\u02D8', '\u02C7', '\u02C6', '\u02D9', '\u02DD', '\u02DB', '\u02DA', '\u02DC' - ]; - - private readonly char[] _pdfDocMappingHigh = - [ - '\u2022', '\u2020', '\u2021', '\u2026', '\u2014', '\u2013', '\u0192', '\u2044', - '\u2039', '\u203A', '\u2212', '\u2030', '\u201E', '\u201C', '\u201D', '\u2018', - '\u2019', '\u201A', '\u2122', '\uFB01', '\uFB02', '\u0141', '\u0152', '\u0160', - '\u0178', '\u017D', '\u0131', '\u0142', '\u0153', '\u0161', '\u017E', ' ', - '\u20AC' - ]; - - private void AppendPdfDocByte(byte b) - { - if (b >= 0x18 && b < 0x20) - { - _builder.Append(_pdfDocMappingLow[b - 0x18]); - } - else if (b >= 0x80 && b < 0xA1) - { - _builder.Append(_pdfDocMappingHigh[b - 0x80]); - } - else - { - _builder.Append((char)b); - } - } - - public void Append(char c) - { - _builder.Append(c); - } - - public void AppendByte(byte b) - { - // PDF Spec 7.9.2.1: Strings are either UTF-16BE or PDFDocEncoded - if (_builder.Length == 0 && !_isUnicode) - { - // Unicode strings are prefixed by a big endian BOM \uFEFF - if (_secondByte) - { - if (b == 0xFF) - { - _isUnicode = true; - _secondByte = false; - } - else - { - AppendPdfDocByte(_prevByte); - AppendPdfDocByte(b); - } - } - else if (!_secondByte && b == 0xFE) - { - _secondByte = true; - _prevByte = b; - } - else - { - AppendPdfDocByte(b); - } - } - else if (_isUnicode) - { - if (_secondByte) - { - _builder.Append((char)(((char)_prevByte) << 8 | (char)b)); - _secondByte = false; - } - else - { - _prevByte = b; - _secondByte = true; - } - } - else - { - AppendPdfDocByte(b); - } - } - - override public string ToString() - { - if (_builder.Length == 0 && _secondByte) - { - AppendPdfDocByte(_prevByte); - } - - return _builder.ToString(); - } -} - -internal class PdfLexer(Stream stream) -{ - private const int BufferSize = 1024; - private readonly byte[] _buffer = new byte[BufferSize]; - private int _pos = 0; - private int _valid = 0; - - public enum TokenType - { - None, - Bool, - Int, - Double, - Name, - String, - ArrayStart, - ArrayEnd, - DictionaryStart, - DictionaryEnd, - StreamStart, - StreamEnd, - ObjectStart, - ObjectEnd, - ObjectRef, - Keyword, - Newline, - } - - public struct Token(TokenType type, object value) - { - public TokenType Type = type; - public object Value = value; - } - - public Token NextToken(bool reportNewlines = false) - { - while (true) - { - switch ((char)NextByte()) - { - case '\n' when reportNewlines: - return new Token(TokenType.Newline, true); - - case '\r' when reportNewlines: - if (NextByte() != '\n') - { - PutBack(); - } - return new Token(TokenType.Newline, true); - - case ' ': - case '\x00': - case '\t': - case '\n': - case '\f': - case '\r': - continue; // Skip whitespace - - case '%': - SkipComment(); - continue; - - case '+': - case '-': - case '.': - case >= '0' and <= '9': - return ScanNumber(); - - case '/': - return ScanName(); - - case '(': - return ScanString(); - - case '[': - return new Token(TokenType.ArrayStart, true); - - case ']': - return new Token(TokenType.ArrayEnd, true); - - case '<': - if (NextByte() == '<') - { - return new Token(TokenType.DictionaryStart, true); - } - else - { - PutBack(); - return ScanHexString(); - } - case '>': - ExpectByte((byte)'>'); - - return new Token(TokenType.DictionaryEnd, true); - - case >= 'a' and <= 'z': - case >= 'A' and <= 'Z': - return ScanKeyword(); - - default: - throw new PdfMetadataExtractorException("Unexpected byte, got {LastByte()}"); - } - } - } - - public void ResetBuffer() - { - _pos = 0; - _valid = 0; - } - - public bool TestByte(byte expected) - { - var result = NextByte() == expected; - - PutBack(); - - return result; - } - - public void ExpectNewline() - { - while (true) - { - var b = NextByte(); - switch ((char)b) - { - case ' ': - case '\t': - case '\f': - continue; // Skip whitespace - - case '\n': - return; - - case '\r': - if (NextByte() != '\n') - { - PutBack(); - } - - return; - - default: - throw new PdfMetadataExtractorException("Unexpected character, expected newline, got {b}"); - } - } - } - - public long GetXRefStart() - { - // Look for the startxref element as per PDF Spec 7.5.5 - while (true) - { - var b = NextByte(); - - switch ((char)b) - { - case '\r': - b = NextByte(); - - if (b != '\n') - { - PutBack(); - } - - goto case '\n'; - - case '\n': - // Handle consecutive newlines - while (true) - { - b = NextByte(); - - if (b == '\r') - { - goto case '\r'; - } - else if (b == '\n') - { - goto case '\n'; - } - else if (b == ' ' || b == '\t' || b == '\f') - { - continue; - } - else - { - PutBack(); - - break; - } - } - - var token = NextToken(true); - - if (token.Type == TokenType.Keyword && (string)token.Value == "startxref") - { - token = NextToken(); - - if (token.Type == TokenType.Int) - { - return (long)token.Value; - } - else - { - throw new PdfMetadataExtractorException("Expected integer after startxref keyword"); - } - } - - continue; - - default: - continue; - } - } - } - - public bool NextXRefEntry(ref long obj, ref int generation) - { - // Cross-reference table entry as per PDF Spec 7.5.4 - - WantLookahead(20); - - if (_valid - _pos < 20) - { - throw new PdfMetadataExtractorException("End of stream"); - } - - var inUse = true; - - if (obj == 0) - { - obj = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, _pos, 10)); - generation = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, _pos + 11, 5)); - inUse = _buffer[_pos + 17] == 'n'; - } - - _pos += 20; - - return inUse; - } - - public Stream StreamObject(int length, bool deflate) - { - // Read a stream object as per PDF Spec 7.3.8 - // At the moment, we only accept uncompressed streams or the FlateDecode (PDF Spec 7.4.1) filter - // with no parameters. These cover the vast majority of streams we're interested in. - - var rawData = new MemoryStream(); - - ExpectNewline(); - - if (_pos < _valid) - { - var buffered = Math.Min(_valid - _pos, length); - rawData.Write(_buffer, _pos, buffered); - length -= buffered; - _pos += buffered; - } - - while (length > 0) - { - var buffered = Math.Min(length, BufferSize); - stream.ReadExactly(_buffer, 0, buffered); - rawData.Write(_buffer, 0, buffered); - _pos = 0; - _valid = 0; - length -= buffered; - } - - rawData.Seek(0, SeekOrigin.Begin); - - if (deflate) - { - return new ZLibStream(rawData, CompressionMode.Decompress, false); - } - else - { - return rawData; - } - } - - private byte NextByte() - { - if (_pos >= _valid) - { - _pos = 0; - _valid = stream.Read(_buffer, 0, BufferSize); - - if (_valid <= 0) - { - throw new PdfMetadataExtractorException("End of stream"); - } - } - - return _buffer[_pos++]; - } - - private byte LastByte() - { - return _buffer[_pos - 1]; - } - - private void PutBack() - { - --_pos; - } - - private void ExpectByte(byte expected) - { - if (NextByte() != expected) - { - throw new PdfMetadataExtractorException($"Unexpected character, expected {expected}"); - } - } - - private void WantLookahead(int length) - { - if (_pos + length > _valid) - { - Buffer.BlockCopy(_buffer, _pos, _buffer, 0, _valid - _pos); - _valid -= _pos; - _pos = 0; - _valid += stream.Read(_buffer, _valid, BufferSize - _valid); - } - } - - private void SkipComment() - { - while (true) - { - var b = NextByte(); - - if (b == '\n') - { - break; - } - else if (b == '\r') - { - if (NextByte() != '\n') - { - PutBack(); - } - - break; - } - } - } - - private Token ScanNumber() - { - StringBuilder sb = new(); - var hasDot = LastByte() == '.'; - var followedBySpace = false; - - sb.Append((char)LastByte()); - - while (true) - { - var b = NextByte(); - - if (b == '.' || b >= '0' && b <= '9') - { - sb.Append((char)b); - - if (b == '.') - { - hasDot = true; - } - } - else - { - followedBySpace = (b == ' ' || b == '\t'); - PutBack(); - - break; - } - } - - if (hasDot) - { - return new Token(TokenType.Double, double.Parse(sb.ToString())); - } - - if (followedBySpace) - { - // Look ahead to see if it's an object reference (PDF Spec 7.3.10) - WantLookahead(32); - - var savedPos = _pos; - var b = NextByte(); - - while (b == ' ' || b == '\t') - { - b = NextByte(); - } - - // Generation number (ignored) - while (b >= '0' && b <= '9') - { - b = NextByte(); - } - - while (b == ' ' || b == '\t') - { - b = NextByte(); - } - - if (b == 'R') - { - return new Token(TokenType.ObjectRef, long.Parse(sb.ToString())); - } - else if (b == 'o' && NextByte() == 'b' && NextByte() == 'j') - { - return new Token(TokenType.ObjectStart, long.Parse(sb.ToString())); - } - else - { - _pos = savedPos; - } - } - - return new Token(TokenType.Int, long.Parse(sb.ToString())); - } - - private static int HexDigit(byte b) - { - return (char) b switch - { - >= '0' and <= '9' => b - (byte) '0', - >= 'a' and <= 'f' => b - (byte) 'a' + 10, - >= 'A' and <= 'F' => b - (byte) 'A' + 10, - _ => throw new PdfMetadataExtractorException("Invalid hex digit, got {b}") - }; - } - - private Token ScanName() - { - // PDF Spec 7.3.5 - - var sb = new StringBuilder(); - while (true) - { - var b = NextByte(); - switch ((char)b) - { - case '(': - case ')': - case '[': - case ']': - case '{': - case '}': - case '<': - case '>': - case '/': - case '%': - PutBack(); - - goto case ' '; - - case ' ': - case '\t': - case '\n': - case '\f': - case '\r': - return new Token(TokenType.Name, sb.ToString()); - - case '#': - var b1 = NextByte(); - var b2 = NextByte(); - b = (byte)((HexDigit(b1) << 4) | HexDigit(b2)); - - goto default; - - default: - sb.Append((char)b); - break; - } - } - } - - private Token ScanString() - { - // PDF Spec 7.3.4.2 - - PdfStringBuilder sb = new(); - var parenLevel = 1; - - while (true) - { - var b = NextByte(); - - switch ((char)b) - { - case '(': - parenLevel++; - - goto default; - - case ')': - if (--parenLevel == 0) - { - return new Token(TokenType.String, sb.ToString()); - } - - goto default; - - case '\\': - b = NextByte(); - - switch ((char)b) - { - case 'b': - sb.Append('\b'); - - break; - - case 'f': - sb.Append('\f'); - - break; - - case 'n': - sb.Append('\n'); - - break; - - case 'r': - sb.Append('\r'); - - break; - - case 't': - sb.Append('\t'); - - break; - - case >= '0' and <= '7': - var b1 = b; - var b2 = NextByte(); - var b3 = NextByte(); - - if (b2 < '0' || b2 > '7' || b3 < '0' || b3 > '7') - { - throw new PdfMetadataExtractorException("Invalid octal escape, got {b1}{b2}{b3}"); - } - - sb.AppendByte((byte)((b1 - '0') << 6 | (b2 - '0') << 3 | (b3 - '0'))); - - break; - } - break; - - default: - sb.AppendByte(b); - break; - } - } - } - - private Token ScanHexString() - { - // PDF Spec 7.3.4.3 - - PdfStringBuilder sb = new(); - - while (true) - { - var b = NextByte(); - - switch ((char)b) - { - case (>= '0' and <= '9') or (>= 'a' and <= 'f') or (>= 'A' and <= 'F'): - var b1 = NextByte(); - if (b1 == '>') - { - PutBack(); - b1 = (byte)'0'; - } - sb.AppendByte((byte)(HexDigit(b) << 4 | HexDigit(b1))); - - break; - - case '>': - return new Token(TokenType.String, sb.ToString()); - - default: - throw new PdfMetadataExtractorException("Invalid hex string, got {b}"); - } - } - } - - private Token ScanKeyword() - { - StringBuilder sb = new(); - - sb.Append((char)LastByte()); - - while (true) - { - var b = NextByte(); - if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) - { - sb.Append((char)b); - } - else - { - PutBack(); - - break; - } - } - - switch (sb.ToString()) - { - case "true": - return new Token(TokenType.Bool, true); - - case "false": - return new Token(TokenType.Bool, false); - - case "stream": - return new Token(TokenType.StreamStart, true); - - case "endstream": - return new Token(TokenType.StreamEnd, true); - - case "endobj": - return new Token(TokenType.ObjectEnd, true); - - default: - return new Token(TokenType.Keyword, sb.ToString()); - } - } -} - -internal class PdfMetadataExtractor : IPdfMetadataExtractor -{ - private readonly ILogger _logger; - private readonly PdfLexer _lexer; - private readonly FileStream _stream; - private long[] _objectOffsets = new long[0]; - private readonly Dictionary _metadata = []; - private readonly Stack _metadataRef = new(); - - private struct MetadataRef(long root, long info) - { - public long Root = root; - public long Info = info; - } - - private struct XRefSection(long first, long count) - { - public readonly long First = first; - public readonly long Count = count; - } - - public PdfMetadataExtractor(ILogger logger, string filename) - { - _logger = logger; - _stream = File.OpenRead(filename); - _lexer = new PdfLexer(_stream); - - ReadObjectOffsets(); - ReadMetadata(filename); - } - - public Dictionary GetMetadata() - { - return _metadata; - } - - private void LogMetadata(string filename) - { - _logger.LogTrace("Metadata for {Path}:", filename); - - foreach (var entry in _metadata) - { - _logger.LogTrace(" {Key:0,-5} : {Value:1}", entry.Key, entry.Value); - } - } - - private void ReadObjectOffsets() - { - // Look for file trailer (PDF Spec 7.5.5) - // Spec says trailer must be strictly at end of file. - // Adobe software accepts trailer within last 1K of EOF, - // but in practice, virtually all PDFs have trailer at end. - - _stream.Seek(-32, SeekOrigin.End); - - var xrefOffset = _lexer.GetXRefStart(); - - ReadXRefAndTrailer(xrefOffset); - } - - private void ReadXRefAndTrailer(long xrefOffset) - { - _stream.Seek(xrefOffset, SeekOrigin.Begin); - _lexer.ResetBuffer(); - - if (!_lexer.TestByte((byte)'x')) - { - // Cross-reference stream (PDF Spec 7.5.8) - - ReadXRefStream(); - - return; - } - - // Cross-reference table (PDF Spec 7.5.4) - - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.Keyword || (string)token.Value != "xref") - { - throw new PdfMetadataExtractorException("Expected xref keyword"); - } - - while (true) - { - token = _lexer.NextToken(); - - if (token.Type == PdfLexer.TokenType.Int) - { - var startObj = (long)token.Value; - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected number of objects in xref subsection"); - } - - var numObj = (long)token.Value; - - if (_objectOffsets.Length < startObj + numObj) - { - Array.Resize(ref _objectOffsets, (int)(startObj + numObj)); - } - - _lexer.ExpectNewline(); - - var generation = 0; - - for (var obj = startObj; obj < startObj + numObj; ++obj) - { - var inUse = _lexer.NextXRefEntry(ref _objectOffsets[obj], ref generation); - - if (!inUse) - { - _objectOffsets[obj] = 0; - } - } - } - else if (token.Type == PdfLexer.TokenType.Keyword && (string)token.Value == "trailer") - { - break; - } - else - { - throw new PdfMetadataExtractorException("Unexpected token in xref"); - } - } - - ReadTrailerDictionary(); - } - - private void ReadXRefStream() - { - // Cross-reference stream (PDF Spec 7.5.8) - - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ObjectStart) - { - throw new PdfMetadataExtractorException("Expected obj keyword"); - } - - long length = -1; - long size = -1; - var deflate = false; - long prev = -1; - long typeWidth = -1; - long offsetWidth = -1; - long generationWidth = -1; - Queue sections = new(); - var meta = new MetadataRef(-1, -1); - - // Cross-reference stream dictionary (PDF Spec 7.5.8.2) - - ParseDictionary(delegate(string key, PdfLexer.Token value) { - switch (key) - { - case "Type": - if (value.Type != PdfLexer.TokenType.Name || (string)value.Value != "XRef") - { - throw new PdfMetadataExtractorException("Expected /Type to be /XRef"); - } - - return true; - - case "Length": - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer after /Length"); - } - - length = (long)value.Value; - - return true; - - case "Size": - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer after /Size"); - } - - size = (long)value.Value; - - return true; - - case "Prev": - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected offset after /Prev"); - } - - prev = (long)value.Value; - - return true; - - case "Index": - if (value.Type != PdfLexer.TokenType.ArrayStart) - { - throw new PdfMetadataExtractorException("Expected array after /Index"); - } - - while (true) - { - token = _lexer.NextToken(); - - if (token.Type == PdfLexer.TokenType.ArrayEnd) - { - break; - } - else if (token.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer in /Index array"); - } - - var first = (long)token.Value; - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer pair in /Index array"); - } - - var count = (long)token.Value; - sections.Enqueue(new XRefSection(first, count)); - } - - return true; - - case "W": - if (value.Type != PdfLexer.TokenType.ArrayStart) - { - throw new PdfMetadataExtractorException("Expected array after /W"); - } - - var widths = new long[3]; - - for (var i = 0; i < 3; ++i) - { - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer in /W array"); - } - - widths[i] = (long)token.Value; - } - - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ArrayEnd) - { - throw new PdfMetadataExtractorException("Unclosed array after /W"); - } - - typeWidth = widths[0]; - offsetWidth = widths[1]; - generationWidth = widths[2]; - - return true; - - case "Filter": - if (value.Type != PdfLexer.TokenType.Name) - { - throw new PdfMetadataExtractorException("Expected name after /Filter"); - } - - if ((string)value.Value != "FlateDecode") - { - throw new PdfMetadataExtractorException("Unsupported filter, only FlateDecode is supported"); - } - - deflate = true; - - return true; - - case "Root": - if (value.Type != PdfLexer.TokenType.ObjectRef) - { - throw new PdfMetadataExtractorException("Expected object reference after /Root"); - } - - meta.Root = (long)value.Value; - - return true; - - case "Info": - if (value.Type != PdfLexer.TokenType.ObjectRef) - { - throw new PdfMetadataExtractorException("Expected object reference after /Info"); - } - - meta.Info = (long)value.Value; - - return true; - - default: - return false; - } - }); - - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.StreamStart) - { - throw new PdfMetadataExtractorException("Expected xref stream after dictionary"); - } - - var stream = _lexer.StreamObject((int)length, deflate); - - if (sections.Count == 0) - { - sections.Enqueue(new XRefSection(0, size)); - } - - while (sections.Count > 0) - { - var section = sections.Dequeue(); - - if (_objectOffsets.Length < size) - { - Array.Resize(ref _objectOffsets, (int)size); - } - - for (var i = section.First; i < section.First + section.Count; ++i) - { - long type = 0; - long offset = 0; - long generation = 0; - - if (typeWidth == 0) - { - type = 1; - } - - for (var j = 0; j < typeWidth; ++j) - { - type = (type << 8) | (ushort)stream.ReadByte(); - } - - for (var j = 0; j < offsetWidth; ++j) - { - offset = (offset << 8) | (ushort)stream.ReadByte(); - } - - for (var j = 0; j < generationWidth; ++j) - { - generation = (generation << 8) | (ushort)stream.ReadByte(); - } - - if (type == 1 && _objectOffsets[i] == 0) - { - _objectOffsets[i] = offset; - } - } - } - - if (prev > -1) - { - ReadXRefAndTrailer(prev); - } - - PushMetadataRef(meta); - } - - private void PushMetadataRef(MetadataRef meta) - { - if (_metadataRef.Count > 0) - { - if (meta.Root == _metadataRef.Peek().Root) - { - meta.Root = -1; - } - - if (meta.Info == _metadataRef.Peek().Info) - { - meta.Info = -1; - } - } - - if (meta.Root != -1 || meta.Info != -1) - { - _metadataRef.Push(meta); - } - } - - private void ReadTrailerDictionary() - { - // Read trailer directory (PDF Spec 7.5.5) - - long prev = -1; - long xrefStm = -1; - - MetadataRef meta = new(-1, -1); - - ParseDictionary(delegate(string key, PdfLexer.Token value) - { - switch (key) - { - case "Root": - if (value.Type != PdfLexer.TokenType.ObjectRef) - { - throw new PdfMetadataExtractorException("Expected object reference after /Root"); - } - - meta.Root = (long)value.Value; - - return true; - case "Prev": - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected offset after /Prev"); - } - - prev = (long)value.Value; - - return true; - case "Info": - if (value.Type != PdfLexer.TokenType.ObjectRef) - { - throw new PdfMetadataExtractorException("Expected object reference after /Info"); - } - - meta.Info = (long)value.Value; - - return true; - case "XRefStm": - // Prefer encoded xref stream over xref table - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected offset after /XRefStm"); - } - - xrefStm = (long)value.Value; - - return true; - - case "Encrypt": - throw new PdfMetadataExtractorException("Encryption not supported"); - - default: - return false; - } - }); - - PushMetadataRef(meta); - - if (xrefStm != -1) - { - ReadXRefAndTrailer(xrefStm); - } - - if (prev != -1) - { - ReadXRefAndTrailer(prev); - } - } - - private void ReadMetadata(string filename) - { - // We read potential metadata sources in backwards historical order, so - // we can overwrite to our heart's content - - while (_metadataRef.Count > 0) - { - var meta = _metadataRef.Pop(); - - //_logger.LogTrace("DocumentCatalog for {Path}: {Root}, Info: {Info}", filename, meta.root, meta.info); - - ReadMetadataFromInfo(meta.Info); - ReadMetadataFromXml(MetadataObjInObjectCatalog(meta.Root)); - } - } - - private void ReadMetadataFromInfo(long infoObj) - { - // Document information dictionary (PDF Spec 14.3.3) - // We treat this as less authoritative than the Metadata stream. - - if (infoObj < 1 || infoObj >= _objectOffsets.Length || _objectOffsets[infoObj] == 0) - { - return; - } - - _stream.Seek(_objectOffsets[infoObj], SeekOrigin.Begin); - _lexer.ResetBuffer(); - - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ObjectStart) - { - throw new PdfMetadataExtractorException("Expected object header"); - } - - Dictionary indirectObjects = []; - - ParseDictionary(delegate(string key, PdfLexer.Token value) - { - switch (key) - { - case "Title": - case "Author": - case "Subject": - case "Keywords": - case "Creator": - case "Producer": - case "CreationDate": - case "ModDate": - if (value.Type == PdfLexer.TokenType.ObjectRef) { - indirectObjects[key] = (long)value.Value; - } - else if (value.Type != PdfLexer.TokenType.String) - { - throw new PdfMetadataExtractorException("Expected string value"); - } - else - { - _metadata[key] = (string)value.Value; - } - - return true; - - default: - return false; - } - }); - - // Resolve indirectly referenced values - foreach(var key in indirectObjects.Keys) { - _stream.Seek(_objectOffsets[indirectObjects[key]], SeekOrigin.Begin); - _lexer.ResetBuffer(); - - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ObjectStart) { - throw new PdfMetadataExtractorException("Expected object here"); - } - - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.String) { - throw new PdfMetadataExtractorException("Expected string"); - } - - _metadata[key] = (string) token.Value; - } - } - - private long MetadataObjInObjectCatalog(long rootObj) - { - // Look for /Metadata entry in document catalog (PDF Spec 7.7.2) - - if (rootObj < 1 || rootObj >= _objectOffsets.Length || _objectOffsets[rootObj] == 0) - { - return -1; - } - - _stream.Seek(_objectOffsets[rootObj], SeekOrigin.Begin); - _lexer.ResetBuffer(); - - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ObjectStart) - { - throw new PdfMetadataExtractorException("Expected object header"); - } - - long meta = -1; - - ParseDictionary(delegate(string key, PdfLexer.Token value) - { - switch (key) { - case "Metadata": - if (value.Type != PdfLexer.TokenType.ObjectRef) - { - throw new PdfMetadataExtractorException("Expected object number after /Metadata"); - } - - meta = (long)value.Value; - - return true; - - default: - return false; - } - }); - - return meta; - } - - // Obtain metadata from XMP stream object - // See XMP specification: https://developer.adobe.com/xmp/docs/XMPSpecifications/ - // and Dublin Core: https://www.dublincore.org/specifications/dublin-core/ - - private static string? GetTextFromXmlNode(XmlDocument doc, XmlNamespaceManager ns, string path) - { - return (doc.DocumentElement?.SelectSingleNode(path + "//rdf:li", ns) - ?? doc.DocumentElement?.SelectSingleNode(path, ns))?.InnerText; - } - - private static string? GetListFromXmlNode(XmlDocument doc, XmlNamespaceManager ns, string path) - { - var nodes = doc.DocumentElement?.SelectNodes(path + "//rdf:li", ns); - - if (nodes == null) return null; - - var list = new StringBuilder(); - - foreach (XmlNode n in nodes) - { - if (list.Length > 0) - { - list.Append(','); - } - - list.Append(n.InnerText); - } - - return list.Length > 0 ? list.ToString() : null; - } - - private void SetMetadata(string key, string? value) - { - if (value == null) return; - - _metadata[key] = value; - } - - private void ReadMetadataFromXml(long meta) - { - if (meta < 1 || meta >= _objectOffsets.Length || _objectOffsets[meta] == 0) return; - - _stream.Seek(_objectOffsets[meta], SeekOrigin.Begin); - _lexer.ResetBuffer(); - - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.ObjectStart) - { - throw new PdfMetadataExtractorException("Expected object header"); - } - - long length = -1; - var deflate = false; - - // Metadata stream dictionary (PDF Spec 14.3.2) - - ParseDictionary(delegate(string key, PdfLexer.Token value) - { - switch (key) { - case "Type": - if (value.Type != PdfLexer.TokenType.Name || (string)value.Value != "Metadata") - { - throw new PdfMetadataExtractorException("Expected /Type to be /Metadata"); - } - - return true; - - case "Subtype": - if (value.Type != PdfLexer.TokenType.Name || (string)value.Value != "XML") - { - throw new PdfMetadataExtractorException("Expected /Subtype to be /XML"); - } - - return true; - - case "Length": - if (value.Type != PdfLexer.TokenType.Int) - { - throw new PdfMetadataExtractorException("Expected integer after /Length"); - } - - length = (long)value.Value; - - return true; - - case "Filter": - if (value.Type != PdfLexer.TokenType.Name) - { - throw new PdfMetadataExtractorException("Expected name after /Filter"); - } - - if ((string)value.Value != "FlateDecode") - { - throw new PdfMetadataExtractorException("Unsupported filter, only FlateDecode is supported"); - } - - deflate = true; - - return true; - - default: - return false; - } - }); - - token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.StreamStart) - { - throw new PdfMetadataExtractorException("Expected xref stream after dictionary"); - } - - var xmlStream = _lexer.StreamObject((int)length, deflate); - - // Skip XMP header - while (true) { - var b = xmlStream.ReadByte(); - - if (b < 0) { - throw new PdfMetadataExtractorException("Reached EOF in XMP header"); - } - - if (b == '?') { - while (b == '?') { - b = xmlStream.ReadByte(); - } - - if (b == '>') { - break; - } - } - } - - var metaDoc = new XmlDocument(); - metaDoc.Load(xmlStream); - - var ns = new XmlNamespaceManager(metaDoc.NameTable); - ns.AddNamespace("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); - ns.AddNamespace("dc", "http://purl.org/dc/elements/1.1/"); - ns.AddNamespace("calibreSI", "http://calibre-ebook.com/xmp-namespace-series-index"); - ns.AddNamespace("calibre", "http://calibre-ebook.com/xmp-namespace"); - ns.AddNamespace("pdfx", "http://ns.adobe.com/pdfx/1.3/"); - ns.AddNamespace("prism", "http://prismstandard.org/namespaces/basic/2.0/"); - ns.AddNamespace("xmp", "http://ns.adobe.com/xap/1.0/"); - - SetMetadata("CreationDate", - GetTextFromXmlNode(metaDoc, ns, "//dc:date") - ?? GetTextFromXmlNode(metaDoc, ns, "//xmp:CreateDate")); - SetMetadata("Summary", GetTextFromXmlNode(metaDoc, ns, "//dc:description")); - SetMetadata("Publisher", GetTextFromXmlNode(metaDoc, ns, "//dc:publisher")); - SetMetadata("Author", GetListFromXmlNode(metaDoc, ns, "//dc:creator")); - SetMetadata("Title", GetTextFromXmlNode(metaDoc, ns, "//dc:title")); - SetMetadata("Subject", GetListFromXmlNode(metaDoc, ns, "//dc:subject")); - SetMetadata("Language", GetTextFromXmlNode(metaDoc, ns, "//dc:language")); - SetMetadata("ISBN", GetTextFromXmlNode(metaDoc, ns, "//pdfx:isbn") ?? GetTextFromXmlNode(metaDoc, ns, "//prism:isbn")); - SetMetadata("UserRating", GetTextFromXmlNode(metaDoc, ns, "//calibre:rating")); - SetMetadata("TitleSort", GetTextFromXmlNode(metaDoc, ns, "//calibre:title_sort")); - SetMetadata("Series", GetTextFromXmlNode(metaDoc, ns, "//calibre:series/rdf:value")); - SetMetadata("Volume", GetTextFromXmlNode(metaDoc, ns, "//calibreSI:series_index")); - } - - private delegate bool DictionaryHandler(string key, PdfLexer.Token value); - - private void ParseDictionary(DictionaryHandler handler) - { - var token = _lexer.NextToken(); - - if (token.Type != PdfLexer.TokenType.DictionaryStart) - { - throw new PdfMetadataExtractorException("Expected dictionary"); - } - - while (true) - { - token = _lexer.NextToken(); - - if (token.Type == PdfLexer.TokenType.DictionaryEnd) - { - return; - } - - if (token.Type == PdfLexer.TokenType.Name) - { - var value = _lexer.NextToken(); - - if (!handler((string)token.Value, value)) { - SkipValue(value); - } - } - else - { - throw new PdfMetadataExtractorException("Improper token in dictionary"); - } - } - } - - private void SkipValue(PdfLexer.Token? existingToken = null) - { - var token = existingToken ?? _lexer.NextToken(); - - switch (token.Type) - { - case PdfLexer.TokenType.Bool: - case PdfLexer.TokenType.Int: - case PdfLexer.TokenType.Double: - case PdfLexer.TokenType.Name: - case PdfLexer.TokenType.String: - case PdfLexer.TokenType.ObjectRef: - break; - case PdfLexer.TokenType.ArrayStart: - { - SkipArray(); - break; - } - case PdfLexer.TokenType.DictionaryStart: - { - SkipDictionary(); - break; - } - default: - throw new PdfMetadataExtractorException("Unexpected token in SkipValue"); - } - } - - private void SkipArray() - { - while (true) - { - var token = _lexer.NextToken(); - - if (token.Type == PdfLexer.TokenType.ArrayEnd) - { - break; - } - - SkipValue(token); - } - } - - private void SkipDictionary() - { - while (true) - { - var token = _lexer.NextToken(); - - if (token.Type == PdfLexer.TokenType.DictionaryEnd) - { - break; - } - if (token.Type != PdfLexer.TokenType.Name) - { - throw new PdfMetadataExtractorException("Expected name in dictionary"); - } - - SkipValue(); - } - } -} diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index b71ff2c1a..f8974a566 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -1,241 +1,165 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using API.Data; using API.DTOs; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; namespace API.Helpers; -#nullable enable -// This isn't needed in the new person architecture public static class PersonHelper { - - public static Dictionary ConstructNameAndAliasDictionary(IList people) + /// + /// Given a list of all existing people, this will check the new names and roles and if it doesn't exist in allPeople, will create and + /// add an entry. For each person in name, the callback will be executed. + /// + /// This does not remove people if an empty list is passed into names + /// This is used to add new people to a list without worrying about duplicating rows in the DB + /// + /// + /// + /// + public static void UpdatePeople(ICollection allPeople, IEnumerable names, PersonRole role, Action action) { - var dict = new Dictionary(); - foreach (var person in people) + // TODO: Validate if we need this, not used + var allPeopleTypeRole = allPeople.Where(p => p.Role == role).ToList(); + + foreach (var name in names) { - dict.TryAdd(person.NormalizedName, person); - foreach (var alias in person.Aliases) + var normalizedName = name.ToNormalized(); + var person = allPeopleTypeRole.Find(p => + p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); + if (person == null) { - dict.TryAdd(alias.NormalizedAlias, person); + person = new PersonBuilder(name, role).Build(); + allPeople.Add(person); } + + action(person); } - return dict; } - public static async Task UpdateSeriesMetadataPeopleAsync(SeriesMetadata metadata, ICollection metadataPeople, - IEnumerable chapterPeople, PersonRole role, IUnitOfWork unitOfWork) + /// + /// Remove people on a list for a given role + /// + /// Used to remove before we update/add new people + /// Existing people on Entity + /// People from metadata + /// Role to filter on + /// Callback which will be executed for each person removed + public static void RemovePeople(ICollection existingPeople, IEnumerable people, PersonRole role, Action? action = null) { - var modification = false; - - // Get all people with the specified role from chapterPeople - var peopleToAdd = chapterPeople - .Where(cp => cp.Role == role) - .Select(cp => new { cp.Person.Name, cp.Person.NormalizedName }) // Store both real and normalized names - .ToList(); - - // Prepare a HashSet for quick lookup of normalized names of people to add - var peopleToAddSet = new HashSet(peopleToAdd.Select(p => p.NormalizedName)); - - // Get all existing people from metadataPeople with the specified role - var existingMetadataPeople = metadataPeople - .Where(mp => mp.Role == role) - .ToList(); - - // Identify people to remove from metadataPeople - var peopleToRemove = existingMetadataPeople - .Where(person => - !peopleToAddSet.Contains(person.Person.NormalizedName) && - !person.Person.Aliases.Any(pa => peopleToAddSet.Contains(pa.NormalizedAlias))) - .ToList(); - - // Remove identified people from metadataPeople - foreach (var personToRemove in peopleToRemove) + var normalizedPeople = people.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList(); + if (normalizedPeople.Count == 0) { - metadataPeople.Remove(personToRemove); - modification = true; + var peopleToRemove = existingPeople.Where(p => p.Role == role).ToList(); + foreach (var existingRoleToRemove in peopleToRemove) + { + existingPeople.Remove(existingRoleToRemove); + action?.Invoke(existingRoleToRemove); + } + return; } - // Bulk fetch existing people from the repository based on normalized names - var existingPeopleInDb = await unitOfWork.PersonRepository - .GetPeopleByNames(peopleToAdd.Select(p => p.NormalizedName).ToList()); - - // Prepare a dictionary for quick lookup of existing people by normalized name - var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeopleInDb); - - // Track the people to attach (newly created people) - var peopleToAttach = new List(); - - // Identify new people (not already in metadataPeople) to add - foreach (var personData in peopleToAdd) + foreach (var person in normalizedPeople) { - var personName = personData.Name; - var normalizedPersonName = personData.NormalizedName; + var existingPerson = existingPeople.FirstOrDefault(p => p.Role == role && person.Equals(p.NormalizedName)); + if (existingPerson == null) continue; - // Check if the person already exists in metadataPeople with the specific role - var personAlreadyInMetadata = metadataPeople - .Any(mp => mp.Person.NormalizedName == normalizedPersonName && mp.Role == role); + existingPeople.Remove(existingPerson); + action?.Invoke(existingPerson); + } - if (!personAlreadyInMetadata) + } + + /// + /// Removes all people that are not present in the removeAllExcept list. + /// + /// + /// + /// Callback for all entities that should be removed + public static void KeepOnlySamePeopleBetweenLists(IEnumerable existingPeople, ICollection removeAllExcept, Action? action = null) + { + foreach (var person in existingPeople) + { + var existingPerson = removeAllExcept + .FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName)); + if (existingPerson == null) { - // Check if the person exists in the database - if (!existingPeopleDict.TryGetValue(normalizedPersonName, out var dbPerson)) + action?.Invoke(person); + } + } + } + + /// + /// Adds the person to the list if it's not already in there + /// + /// + /// + public static void AddPersonIfNotExists(ICollection metadataPeople, Person person) + { + if (string.IsNullOrEmpty(person.Name)) return; + var existingPerson = metadataPeople.FirstOrDefault(p => + p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role); + + if (existingPerson == null) + { + metadataPeople.Add(person); + } + } + + + /// + /// For a given role and people dtos, update a series + /// + /// + /// + /// + /// + /// This will call with an existing or new tag, but the method does not update the series Metadata + /// + public static void UpdatePeopleList(PersonRole role, ICollection? people, Series series, IReadOnlyCollection allPeople, + Action handleAdd, Action onModified) + { + if (people == null) return; + var isModified = false; + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList(); + foreach (var existing in existingTags) + { + if (people.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role + { + // Remove tag + series.Metadata.People.Remove(existing); + isModified = true; + } + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tag in people) + { + var existingTag = allPeople.FirstOrDefault(t => t.Name == tag.Name && t.Role == tag.Role); + if (existingTag != null) + { + if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name))) { - // If not, create a new Person entity using the real name - dbPerson = new PersonBuilder(personName).Build(); - peopleToAttach.Add(dbPerson); // Add new person to the list to be attached + handleAdd(existingTag); + isModified = true; } - - // Add the person to the SeriesMetadataPeople collection - metadataPeople.Add(new SeriesMetadataPeople - { - PersonId = dbPerson.Id, // EF Core will automatically update this after attach - Person = dbPerson, - SeriesMetadataId = metadata.Id, - SeriesMetadata = metadata, - Role = role - }); - modification = true; + } + else + { + // Add new tag + handleAdd(new PersonBuilder(tag.Name, role).Build()); + isModified = true; } } - // Attach all new people in one go (EF Core will assign IDs after commit) - if (peopleToAttach.Count != 0) + if (isModified) { - await unitOfWork.DataContext.Person.AddRangeAsync(peopleToAttach); + onModified(); } - - // Commit the changes if any modifications were made - if (modification) - { - await unitOfWork.CommitAsync(); - } - } - - - - public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role, IUnitOfWork unitOfWork) - { - var modification = false; - - // Normalize the input names for comparison - var normalizedPeople = people.Select(p => p.ToNormalized()).Distinct().ToList(); // Ensure distinct people - - // Get all existing ChapterPeople for the role - var existingChapterPeople = chapter.People - .Where(cp => cp.Role == role) - .ToList(); - - // Prepare a hash set for quick lookup of existing people by normalized name - var existingPeopleNames = new HashSet(existingChapterPeople.Select(cp => cp.Person.NormalizedName)); - - // Bulk select all people from the repository whose normalized names are in the provided list - var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedPeople); - - // Prepare a dictionary for quick lookup by normalized name - var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeople); - - // Identify people to remove (those present in ChapterPeople but not in the new list) - var toRemove = existingChapterPeople - .Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName)); - foreach (var existingChapterPerson in toRemove) - { - chapter.People.Remove(existingChapterPerson); - unitOfWork.PersonRepository.Remove(existingChapterPerson); - modification = true; - } - - // Identify new people to add - var newPeopleNames = normalizedPeople - .Where(p => !existingPeopleNames.Contains(p)) - .ToList(); - - if (newPeopleNames.Count > 0) - { - // Bulk insert new people (if they don't already exist in the database) - var newPeople = newPeopleNames - .Where(name => !existingPeopleDict.ContainsKey(name)) // Avoid adding duplicates - .Select(name => - { - var realName = people.First(p => p.ToNormalized() == name); // Get the original name - return new PersonBuilder(realName).Build(); // Use the real name for the Person entity - }) - .ToList(); - - foreach (var newPerson in newPeople) - { - unitOfWork.DataContext.Person.Attach(newPerson); - existingPeopleDict[newPerson.NormalizedName] = newPerson; - } - - await unitOfWork.CommitAsync(); - modification = true; - } - - // Add all people (both existing and newly created) to the ChapterPeople - foreach (var personName in normalizedPeople) - { - var person = existingPeopleDict[personName]; - - // Check if the person with the specific role is already added to the chapter's People collection - if (chapter.People.Any(cp => cp.PersonId == person.Id && cp.Role == role)) continue; - - chapter.People.Add(new ChapterPeople - { - PersonId = person.Id, - ChapterId = chapter.Id, - Role = role - }); - modification = true; - } - - // Commit the changes to remove and add people - if (modification) - { - await unitOfWork.CommitAsync(); - } - } - - - public static bool HasAnyPeople(SeriesMetadataDto? dto) - { - if (dto == null) return false; - return dto.Writers.Count != 0 || - dto.CoverArtists.Count != 0 || - dto.Publishers.Count != 0 || - dto.Characters.Count != 0 || - dto.Pencillers.Count != 0 || - dto.Inkers.Count != 0 || - dto.Colorists.Count != 0 || - dto.Letterers.Count != 0 || - dto.Editors.Count != 0 || - dto.Translators.Count != 0 || - dto.Teams.Count != 0 || - dto.Locations.Count != 0; - } - - public static bool HasAnyPeople(UpdateChapterDto? dto) - { - if (dto == null) return false; - return dto.Writers.Count != 0 || - dto.CoverArtists.Count != 0 || - dto.Publishers.Count != 0 || - dto.Characters.Count != 0 || - dto.Pencillers.Count != 0 || - dto.Inkers.Count != 0 || - dto.Colorists.Count != 0 || - dto.Letterers.Count != 0 || - dto.Editors.Count != 0 || - dto.Translators.Count != 0 || - dto.Teams.Count != 0 || - dto.Locations.Count != 0; } } diff --git a/API/Helpers/RateLimiter.cs b/API/Helpers/RateLimiter.cs deleted file mode 100644 index c89fc2778..000000000 --- a/API/Helpers/RateLimiter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace API.Helpers; - -public class RateLimiter(int maxRequests, TimeSpan duration, bool refillBetween = true) -{ - private readonly Dictionary _tokenBuckets = new(); - private readonly object _lock = new(); - - public bool TryAcquire(string key) - { - lock (_lock) - { - if (!_tokenBuckets.TryGetValue(key, out var bucket)) - { - bucket = (Tokens: maxRequests, LastRefill: DateTime.UtcNow); - _tokenBuckets[key] = bucket; - } - - RefillTokens(key); - - lock (_lock) - { - - if (_tokenBuckets[key].Tokens > 0) - { - _tokenBuckets[key] = (Tokens: _tokenBuckets[key].Tokens - 1, LastRefill: _tokenBuckets[key].LastRefill); - return true; - } - } - - return false; - } - } - - private void RefillTokens(string key) - { - lock (_lock) - { - var now = DateTime.UtcNow; - var timeSinceLastRefill = now - _tokenBuckets[key].LastRefill; - var tokensToAdd = (int) (timeSinceLastRefill.TotalSeconds / duration.TotalSeconds); - - // Refill the bucket if the elapsed time is greater than or equal to the duration - if (timeSinceLastRefill >= duration) - { - _tokenBuckets[key] = (Tokens: maxRequests, LastRefill: now); - Console.WriteLine($"Tokens Refilled to Max: {maxRequests}"); - } - else if (tokensToAdd > 0 && refillBetween) - { - _tokenBuckets[key] = (Tokens: Math.Min(maxRequests, _tokenBuckets[key].Tokens + tokensToAdd), LastRefill: now); - Console.WriteLine($"Tokens Refilled: {_tokenBuckets[key].Tokens}"); - } - } - } -} - diff --git a/API/Helpers/ReadingListHelper.cs b/API/Helpers/ReadingListHelper.cs new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/API/Helpers/ReadingListHelper.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/API/Helpers/ReviewHelper.cs b/API/Helpers/ReviewHelper.cs deleted file mode 100644 index 03c50a4cf..000000000 --- a/API/Helpers/ReviewHelper.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using API.DTOs.SeriesDetail; -using HtmlAgilityPack; - -namespace API.Helpers; - -public static class ReviewHelper -{ - private const int BodyTextLimit = 175; - public static IEnumerable SelectSpectrumOfReviews(IList reviews) - { - IList externalReviews; - var totalReviews = reviews.Count; - - if (totalReviews > 10) - { - var stepSize = Math.Max((totalReviews - 4) / 8, 1); - - var selectedReviews = new List() - { - reviews[0], - reviews[1], - }; - for (var i = 2; i < totalReviews - 2; i += stepSize) - { - selectedReviews.Add(reviews[i]); - - if (selectedReviews.Count >= 8) - break; - } - - selectedReviews.Add(reviews[totalReviews - 2]); - selectedReviews.Add(reviews[totalReviews - 1]); - - externalReviews = selectedReviews; - } - else - { - externalReviews = reviews; - } - - return externalReviews.OrderByDescending(r => r.Score); - } - - public static string GetCharacters(string body) - { - if (string.IsNullOrEmpty(body)) return body; - - var doc = new HtmlDocument(); - doc.LoadHtml(body); - - var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]"); - if (textNodes == null) return string.Empty; - var plainText = string.Join(" ", textNodes - .Select(node => node.InnerText) - .Where(s => !s.Equals("\n"))); - - // Clean any leftover markdown out - plainText = Regex.Replace(plainText, @"\*\*(.*?)\*\*", "$1"); // Bold with ** - plainText = Regex.Replace(plainText, @"_(.*?)_", "$1"); // Italic with _ - plainText = Regex.Replace(plainText, @"\[(.*?)\]\((.*?)\)", "$1"); // Links [text](url) - plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty); - plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty); - plainText = Regex.Replace(plainText, @"~~~(.*?)~~~", "$1"); - plainText = Regex.Replace(plainText, @"\+{3}(.*?)\+{3}", "$1"); - plainText = Regex.Replace(plainText, @"~~(.*?)~~", "$1"); - plainText = Regex.Replace(plainText, @"__(.*?)__", "$1"); - plainText = Regex.Replace(plainText, @"#\s(.*?)", "$1"); - - - // Just strip symbols - plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty); - plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty); - plainText = Regex.Replace(plainText, @"~~~", string.Empty); - plainText = Regex.Replace(plainText, @"\+", string.Empty); - plainText = Regex.Replace(plainText, @"~~", string.Empty); - plainText = Regex.Replace(plainText, @"__", string.Empty); - - // Take the first BodyTextLimit characters - plainText = plainText.Length > BodyTextLimit ? plainText.Substring(0, BodyTextLimit) : plainText; - - return plainText + "…"; - } - -} diff --git a/API/Helpers/SQLHelper.cs b/API/Helpers/SQLHelper.cs new file mode 100644 index 000000000..575ba8c77 --- /dev/null +++ b/API/Helpers/SQLHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Microsoft.EntityFrameworkCore; + +namespace API.Helpers; + +public static class SqlHelper +{ + public static List RawSqlQuery(DbContext context, string query, Func map) + { + using var command = context.Database.GetDbConnection().CreateCommand(); + command.CommandText = query; + command.CommandType = CommandType.Text; + + context.Database.OpenConnection(); + + using var result = command.ExecuteReader(); + var entities = new List(); + + while (result.Read()) + { + entities.Add(map(result)); + } + + return entities; + } +} diff --git a/API/Helpers/SeriesHelper.cs b/API/Helpers/SeriesHelper.cs index 231575b0e..2b520fb7e 100644 --- a/API/Helpers/SeriesHelper.cs +++ b/API/Helpers/SeriesHelper.cs @@ -6,7 +6,6 @@ using API.Extensions; using API.Services.Tasks.Scanner; namespace API.Helpers; -#nullable enable public static class SeriesHelper { diff --git a/API/Helpers/SmartFilterHelper.cs b/API/Helpers/SmartFilterHelper.cs index 8f61fde21..740b8cd4e 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/API/Helpers/SmartFilterHelper.cs @@ -5,24 +5,14 @@ using System.Web; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; -#nullable enable - namespace API.Helpers; public static class SmartFilterHelper { private const string SortOptionsKey = "sortOptions="; - private const string NameKey = "name="; - private const string SortFieldKey = "sortField="; - private const string IsAscendingKey = "isAscending="; private const string StatementsKey = "stmts="; private const string LimitToKey = "limitTo="; private const string CombinationKey = "combination="; - private const string StatementComparisonKey = "comparison="; - private const string StatementFieldKey = "field="; - private const string StatementValueKey = "value="; - public const string StatementSeparator = "\ufffd"; - public const string InnerStatementSeparator = "¦"; public static FilterV2Dto Decode(string? encodedFilter) { @@ -31,7 +21,7 @@ public static class SmartFilterHelper return new FilterV2Dto(); // Create a default filter if the input is empty } - var parts = encodedFilter.Split('&'); + string[] parts = encodedFilter.Split('&'); var filter = new FilterV2Dto(); foreach (var part in parts) @@ -52,7 +42,7 @@ public static class SmartFilterHelper { filter.Statements = DecodeFilterStatementDtos(part.Substring(StatementsKey.Length)); } - else if (part.StartsWith(NameKey)) + else if (part.StartsWith("name=")) { filter.Name = HttpUtility.UrlDecode(part.Substring(5)); } @@ -61,7 +51,7 @@ public static class SmartFilterHelper return filter; } - public static string Encode(FilterV2Dto? filter) + public static string Encode(FilterV2Dto filter) { if (filter == null) return string.Empty; @@ -69,50 +59,50 @@ public static class SmartFilterHelper var encodedStatements = EncodeFilterStatementDtos(filter.Statements); var encodedSortOptions = filter.SortOptions != null ? $"{SortOptionsKey}{EncodeSortOptions(filter.SortOptions)}" - : string.Empty; + : ""; var encodedLimitTo = $"{LimitToKey}{filter.LimitTo}"; return $"{EncodeName(filter.Name)}{encodedStatements}&{encodedSortOptions}&{encodedLimitTo}&{CombinationKey}{(int) filter.Combination}"; } - private static string EncodeName(string? name) + private static string EncodeName(string name) { - return string.IsNullOrWhiteSpace(name) ? string.Empty : $"{NameKey}{Uri.EscapeDataString(name)}&"; + return string.IsNullOrWhiteSpace(name) ? string.Empty : $"name={HttpUtility.UrlEncode(name)}&"; } private static string EncodeSortOptions(SortOptions sortOptions) { - return Uri.EscapeDataString($"{SortFieldKey}{(int) sortOptions.SortField}{InnerStatementSeparator}{IsAscendingKey}{sortOptions.IsAscending}"); + return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField},isAscending={sortOptions.IsAscending}"); } - private static string EncodeFilterStatementDtos(ICollection? statements) + private static string EncodeFilterStatementDtos(ICollection statements) { if (statements == null || statements.Count == 0) return string.Empty; - var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(StatementSeparator, statements.Select(EncodeFilterStatementDto))); + var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(",", statements.Select(EncodeFilterStatementDto))); return encodedStatements; } private static string EncodeFilterStatementDto(FilterStatementDto statement) { + var encodedComparison = $"comparison={(int) statement.Comparison}"; + var encodedField = $"field={(int) statement.Field}"; + var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}"; - var encodedComparison = $"{StatementComparisonKey}{(int) statement.Comparison}"; - var encodedField = $"{StatementFieldKey}{(int) statement.Field}"; - var encodedValue = $"{StatementValueKey}{Uri.EscapeDataString(statement.Value)}"; - - return Uri.EscapeDataString($"{encodedComparison}{InnerStatementSeparator}{encodedField}{InnerStatementSeparator}{encodedValue}"); + return Uri.EscapeDataString($"{encodedComparison},{encodedField},{encodedValue}"); } private static List DecodeFilterStatementDtos(string encodedStatements) { - var statementStrings = Uri.UnescapeDataString(encodedStatements).Split(StatementSeparator); + encodedStatements = HttpUtility.UrlDecode(encodedStatements); + string[] statementStrings = encodedStatements.Split(','); var statements = new List(); foreach (var statementString in statementStrings) { - var parts = Uri.UnescapeDataString(statementString).Split(InnerStatementSeparator); + var parts = statementString.Split('&'); if (parts.Length < 3) continue; @@ -120,7 +110,7 @@ public static class SmartFilterHelper { Comparison = Enum.Parse(parts[0].Split("=")[1]), Field = Enum.Parse(parts[1].Split("=")[1]), - Value = Uri.UnescapeDataString(parts[2].Split("=")[1]) + Value = HttpUtility.UrlDecode(parts[2].Split("=")[1]) }); } @@ -129,23 +119,22 @@ public static class SmartFilterHelper private static SortOptions DecodeSortOptions(string encodedSortOptions) { - var parts = Uri.UnescapeDataString(encodedSortOptions).Split(InnerStatementSeparator); + string[] parts = encodedSortOptions.Split(','); + var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField=")); + var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith("isAscending=")); - var sortFieldPart = Array.Find(parts, part => part.StartsWith(SortFieldKey)); - var isAscendingPart = Array.Find(parts, part => part.StartsWith(IsAscendingKey)); - - var isAscending = isAscendingPart?.Trim().Replace(IsAscendingKey, string.Empty).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; - if (sortFieldPart == null) + var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; + if (sortFieldPart != null) { - return new SortOptions(); + var sortField = Enum.Parse(sortFieldPart.Split("=")[1]); + + return new SortOptions + { + SortField = sortField, + IsAscending = isAscending + }; } - var sortField = Enum.Parse(sortFieldPart.Split("=")[1]); - - return new SortOptions - { - SortField = sortField, - IsAscending = isAscending - }; + return null; } } diff --git a/API/Helpers/StringHelper.cs b/API/Helpers/StringHelper.cs deleted file mode 100644 index 0a20910c5..000000000 --- a/API/Helpers/StringHelper.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.RegularExpressions; - -namespace API.Helpers; -#nullable enable - -public static partial class StringHelper -{ - #region Regex Source Generators - [GeneratedRegex(@"\s?\(Source:\s*[^)]+\)")] - private static partial Regex SourceRegex(); - [GeneratedRegex(@"", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] - private static partial Regex BrStandardizeRegex(); - [GeneratedRegex(@"(?:
\s*)+", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] - private static partial Regex BrMultipleRegex(); - [GeneratedRegex(@"\s+")] - private static partial Regex WhiteSpaceRegex(); - [GeneratedRegex("&#64;")] - private static partial Regex HtmlEncodedAtSymbolRegex(); - #endregion - - /// - /// Used to squash duplicate break and new lines with a single new line. - /// - /// Test br br Test -> Test br Test - /// - /// - public static string? SquashBreaklines(string? summary) - { - if (string.IsNullOrWhiteSpace(summary)) - { - return null; - } - - // First standardize all br tags to
format - summary = BrStandardizeRegex().Replace(summary, "
"); - - // Replace multiple consecutive br tags with a single br tag - summary = BrMultipleRegex().Replace(summary, "
"); - - // Normalize remaining whitespace (replace multiple spaces with a single space) - summary = WhiteSpaceRegex().Replace(summary, " ").Trim(); - - return summary.Trim(); - } - - /// - /// Removes the (Source: MangaDex) type of tags at the end of descriptions from AL - /// - /// - /// - public static string? RemoveSourceInDescription(string? description) - { - if (string.IsNullOrEmpty(description)) return description; - - return SourceRegex().Replace(description, string.Empty).Trim(); - } - - /// - /// Replaces some HTML encoded characters in urls with the proper symbol. This is common in People Description's - /// - /// - /// - public static string? CorrectUrls(string? description) - { - if (string.IsNullOrEmpty(description)) return description; - - return HtmlEncodedAtSymbolRegex().Replace(description, "@"); - } -} diff --git a/API/Helpers/TagHelper.cs b/API/Helpers/TagHelper.cs index c00d6ee8f..492214a34 100644 --- a/API/Helpers/TagHelper.cs +++ b/API/Helpers/TagHelper.cs @@ -1,162 +1,146 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; -using System.Threading.Tasks; using API.Data; using API.DTOs.Metadata; using API.Entities; using API.Extensions; using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.EntityFrameworkCore; namespace API.Helpers; -#nullable enable +#nullable enable public static class TagHelper { - - public static async Task UpdateChapterTags(Chapter chapter, IEnumerable tagNames, IUnitOfWork unitOfWork) + /// + /// + /// + /// + /// + /// Callback for every item. Will give said item back and a bool if item was added + public static void UpdateTag(ICollection allTags, IEnumerable names, Action action) { - // Normalize tag names once and store them in a hash set for quick lookups - // Create a dictionary: normalized => original - var normalizedToOriginal = tagNames - .Select(t => new { Original = t, Normalized = t.ToNormalized() }) - .GroupBy(x => x.Normalized) // in case of duplicates - .ToDictionary(g => g.Key, g => g.First().Original); - - var normalizedTagsToAdd = new HashSet(normalizedToOriginal.Keys); - var existingTagsSet = new HashSet(chapter.Tags.Select(t => t.NormalizedTitle)); - - var isModified = false; - - // Remove tags that are no longer present in the new list - var tagsToRemove = chapter.Tags - .Where(t => !normalizedTagsToAdd.Contains(t.NormalizedTitle)) - .ToList(); - - if (tagsToRemove.Count != 0) + foreach (var name in names) { - foreach (var tagToRemove in tagsToRemove) + if (string.IsNullOrEmpty(name.Trim())) continue; + + var added = false; + var normalizedName = name.ToNormalized(); + + var genre = allTags.FirstOrDefault(p => + p.NormalizedTitle.Equals(normalizedName)); + if (genre == null) { - chapter.Tags.Remove(tagToRemove); + added = true; + genre = new TagBuilder(name).Build(); + allTags.Add(genre); } - isModified = true; + + action(genre, added); + } + } + + public static void KeepOnlySameTagBetweenLists(ICollection existingTags, ICollection removeAllExcept, Action? action = null) + { + var existing = existingTags.ToList(); + foreach (var genre in existing) + { + var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle.Equals(g.NormalizedTitle)); + if (existingPerson != null) continue; + existingTags.Remove(genre); + action?.Invoke(genre); } - // Get all normalized titles for bulk lookup from the database - var existingTagTitles = await unitOfWork.DataContext.Tag - .Where(t => normalizedTagsToAdd.Contains(t.NormalizedTitle)) - .ToDictionaryAsync(t => t.NormalizedTitle); + } - // Find missing tags that are not already in the database - var missingTags = normalizedTagsToAdd - .Where(nt => !existingTagTitles.ContainsKey(nt)) - .Select(nt => new TagBuilder(normalizedToOriginal[nt]).Build()) - .ToList(); - - // Add missing tags to the database if any - if (missingTags.Count != 0) + /// + /// Adds the tag to the list if it's not already in there. This will ignore the ExternalTag. + /// + /// + /// + public static void AddTagIfNotExists(ICollection metadataTags, Tag tag) + { + var existingGenre = metadataTags.FirstOrDefault(p => + p.NormalizedTitle == tag.Title.ToNormalized()); + if (existingGenre == null) { - unitOfWork.DataContext.Tag.AddRange(missingTags); - await unitOfWork.CommitAsync(); // Commit once after adding missing tags to avoid multiple DB calls - isModified = true; - - // Update the dictionary with newly inserted tags for easier lookup - foreach (var tag in missingTags) - { - existingTagTitles[tag.NormalizedTitle] = tag; - } + metadataTags.Add(tag); } + } - // Add the new or existing tags to the chapter - foreach (var normalizedTitle in normalizedTagsToAdd) + public static void AddTagIfNotExists(BlockingCollection metadataTags, Tag tag) + { + var existingGenre = metadataTags.FirstOrDefault(p => + p.NormalizedTitle == tag.Title.ToNormalized()); + if (existingGenre == null) { - if (existingTagsSet.Contains(normalizedTitle)) continue; - - var tag = existingTagTitles[normalizedTitle]; - chapter.Tags.Add(tag); - isModified = true; - } - - // Commit changes if modifications were made to the chapter's tags - if (isModified) - { - await unitOfWork.CommitAsync(); + metadataTags.Add(tag); } } /// - /// Returns a list of strings separated by ',', distinct by normalized names, already trimmed and empty entries removed. + /// Remove tags on a list /// - /// - /// - public static IList GetTagValues(string comicInfoTagSeparatedByComma) + /// Used to remove before we update/add new tags + /// Existing tags on Entity + /// Tags from metadata + /// Callback which will be executed for each tag removed + public static void RemoveTags(ICollection existingTags, IEnumerable tags, Action? action = null) { - // TODO: Refactor this into an Extension - if (string.IsNullOrEmpty(comicInfoTagSeparatedByComma)) + var normalizedTags = tags.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList(); + foreach (var person in normalizedTags) { - return ImmutableList.Empty; + var existingTag = existingTags.FirstOrDefault(p => person.Equals(p.NormalizedTitle)); + if (existingTag == null) continue; + + existingTags.Remove(existingTag); + action?.Invoke(existingTag); } - return comicInfoTagSeparatedByComma.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .DistinctBy(Parser.Normalize) - .ToList(); } - - public static void UpdateTagList(ICollection? existingDbTags, Series series, IReadOnlyCollection newTags, Action handleAdd, Action onModified) + public static void UpdateTagList(ICollection? tags, Series series, IReadOnlyCollection allTags, Action handleAdd, Action onModified) { - UpdateTagList((existingDbTags ?? []).Select(t => t.Title).ToList(), series, newTags, handleAdd, onModified); - } - - public static void UpdateTagList(ICollection? existingDbTags, Series series, IReadOnlyCollection newTags, Action handleAdd, Action onModified) - { - if (existingDbTags == null) return; + if (tags == null) return; var isModified = false; - - // Convert tags and existing genres to hash sets for quick lookups by normalized title - var existingTagSet = new HashSet(existingDbTags.Select(t => t.ToNormalized())); - var dbTagSet = new HashSet(series.Metadata.Tags.Select(g => g.NormalizedTitle)); - - // Remove tags that are no longer present in the input tags - var existingTagsCopy = series.Metadata.Tags.ToList(); // Copy to avoid modifying collection while iterating - foreach (var existing in existingTagsCopy) + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.Tags.ToList(); + foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null)) { - if (!existingTagSet.Contains(existing.NormalizedTitle)) // This correctly ensures removal of non-present tags + // Remove tag + series.Metadata.Tags.Remove(existing); + isModified = true; + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tagTitle in tags.Select(t => t.Title)) + { + var normalizedTitle = tagTitle.ToNormalized(); + var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle)); + if (existingTag != null) { - series.Metadata.Tags.Remove(existing); + if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle)) + { + + handleAdd(existingTag); + isModified = true; + } + } + else + { + // Add new tag + handleAdd(new TagBuilder(tagTitle).Build()); isModified = true; } } - // Prepare a dictionary for quick lookup of genres from the `newTags` collection by normalized title - var allTagsDict = newTags.ToDictionary(t => t.NormalizedTitle); - - // Add new tags from the input list - foreach (var tagDto in existingDbTags) - { - var normalizedTitle = tagDto.ToNormalized(); - - if (dbTagSet.Contains(normalizedTitle)) continue; // This prevents re-adding existing genres - - if (allTagsDict.TryGetValue(normalizedTitle, out var existingTag)) - { - handleAdd(existingTag); // Add existing tag from allTagsDict - } - else - { - handleAdd(new TagBuilder(tagDto).Build()); // Add new genre if not found - } - isModified = true; - } - - // Call onModified if any changes were made if (isModified) { onModified(); } } } + +#nullable disable diff --git a/API/Helpers/UserParams.cs b/API/Helpers/UserParams.cs index 525f9340c..e5eb37802 100644 --- a/API/Helpers/UserParams.cs +++ b/API/Helpers/UserParams.cs @@ -1,5 +1,4 @@ namespace API.Helpers; -#nullable enable public class UserParams { @@ -16,7 +15,7 @@ public class UserParams init => _pageSize = (value == 0) ? MaxPageSize : value; } - public static readonly UserParams Default = new() + public static readonly UserParams Default = new UserParams() { PageSize = 20, PageNumber = 1 diff --git a/API/I18N/ca.json b/API/I18N/ca.json deleted file mode 100644 index b314a9374..000000000 --- a/API/I18N/ca.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "confirm-email": "Heu de confirmar l'adreça electrònica primer", - "invalid-password": "La contrasenya no és vàlida", - "nothing-to-do": "Res a fer", - "no-user": "El compte no existeix", - "invalid-token": "El testimoni no és vàlid", - "volume-num": "Volum {0}", - "book-num": "Llibre {0}", - "issue-num": "Número {0}{1}", - "chapter-num": "Capítol {0}", - "check-updates": "Comprova si hi ha actualitzacions", - "invalid-username": "El nom d'usuari no és vàlid", - "chapter-doesnt-exist": "El capítol no existeix", - "collection-updated": "S'ha actualitzat la col·lecció correctament", - "collection-deleted": "S'ha suprimit la col·lecció", - "collection-doesnt-exist": "La col·lecció no existeix", - "collection-already-exists": "La col·lecció ja existeix", - "person-doesnt-exist": "La persona no existeix", - "device-doesnt-exist": "El dispositiu no existeix", - "volume-doesnt-exist": "El volum no existeix", - "series-doesnt-exist": "La sèrie no existeix", - "no-cover-image": "No hi ha cap imatge de coberta", - "library-name-exists": "El nom de la biblioteca ja existeix. Trieu un nom únic per al servidor.", - "generic-library": "S'ha produït un error greu. Torneu-ho a provar.", - "invalid-filename": "El nom de fitxer no és vàlid", - "file-doesnt-exist": "El fitxer no existeix", - "user-doesnt-exist": "El compte no existeix", - "no-library-access": "El compte no té accés a aquesta biblioteca", - "library-doesnt-exist": "La biblioteca no existeix", - "invalid-path": "El camí no és vàlid", - "libraries": "Totes les biblioteques", - "browse-libraries": "Explora per biblioteques", - "collections": "Totes les col·leccions", - "browse-collections": "Explora per col·leccions", - "smart-filters": "Filtres intel·ligents", - "external-sources": "Fonts externes", - "browse-external-sources": "Explora les fonts externes", - "search": "Cerca", - "search-description": "Cerca sèries, col·leccions o llistes de lectura", - "external-source-required": "Cal la clau de l'API i l'amfitrió", - "smart-filter-doesnt-exist": "El filtre intel·ligent no existeix", - "collection-tag-duplicate": "Ja existeix una col·lecció amb aquest nom", - "device-duplicate": "Ja existeix un dispositiu amb aquest nom", - "send-to-permission": "No és possible enviar fitxers que no siguin EPUB o PDF perquè el Kindle no els admet", - "browse-smart-filters": "Explora per filtres intel·ligents", - "external-source-already-exists": "La font externa ja existeix", - "device-not-created": "Aquest dispositiu no existeix encara. Creeu-lo primer", - "external-source-doesnt-exist": "La font externa no existeix", - "backup": "Còpia de seguretat", - "file-missing": "No s'ha trobat el fitxer al llibre", - "reading-list-deleted": "S'ha suprimit la llista de lectura", - "generic-device-delete": "S'ha produït un error en suprimir el dispositiu", - "reading-list-position": "No s'ha pogut actualitzar la posició", - "generic-reading-list-delete": "S'ha produït un problema en suprimir la llista de lectura", - "generic-device-create": "S'ha produït un error en crear el dispositiu", - "generic-device-update": "S'ha produït un error en actualitzar el dispositiu", - "reading-list-doesnt-exist": "La llista de lectura no existeix", - "update-metadata-fail": "No s'han pogut actualitzar les metadades", - "reading-list-name-exists": "Ja existeix una llista de lectura amb aquest nom", - "ip-address-invalid": "L'adreça IP «{0}» no és vàlida", - "reading-list-item-delete": "No s'han pogut suprimir els elements", - "generic-reading-list-create": "S'ha produït un problema en crear la llista de lectura", - "generic-reading-list-update": "S'ha produït un problema en actualitzar la llista de lectura", - "generic-create-temp-archive": "S'ha produït un problema en crear l'arxiu temporal" -} diff --git a/API/I18N/cs.json b/API/I18N/cs.json index e136d8e75..79b614c25 100644 --- a/API/I18N/cs.json +++ b/API/I18N/cs.json @@ -18,6 +18,7 @@ "age-restriction-update": "Při aktualizaci věkového omezení došlo k chybě", "no-user": "Uživatel neexistuje", "username-taken": "Uživatelské jméno je již používáno", + "bad-credentials": "Vaše přihlašovací údaje nejsou správné", "invalid-password": "Neplatné heslo", "invalid-token": "Neplatný token", "user-already-confirmed": "Uživatel je již potvrzen", @@ -49,7 +50,7 @@ "generic-device-update": "Při aktualizaci zařízení došlo k chybě", "generic-device-delete": "Při mazání zařízení došlo k chybě", "greater-0": "{0} musí být větší než 0", - "send-to-kavita-email": "Odeslat do zařízení nelze použít s e-mailovou službou Kavita. Nakonfigurujte si prosím vlastní", + "send-to-kavita-email": "Odeslat do zařízení nelze použít s e-mailovou službou Kavita. Nakonfigurujte si prosím vlastní.", "send-to-device-status": "Přenos souborů do vašeho zařízení", "generic-send-to": "Při odesílání souborů do zařízení došlo k chybě", "admin-already-exists": "Správce již existuje", @@ -158,56 +159,5 @@ "send-to-permission": "Nelze odeslat non-EPUB nebo PDF do zařízení, která nejsou podporována na Kindle", "reading-list-title-required": "Název seznamu čtení nemůže být prázdný", "series-restricted-age-restriction": "Uživatel nemá povoleno sledovat tuto sérii z důvodu věkového omezení", - "collection-deleted": "Sbírka smazána", - "smart-filter-already-in-use": "S tímto chytrým filtrem již existuje stream", - "smart-filters": "Chytré filtry", - "browse-smart-filters": "Prohlížet podle chytrých filtrů", - "smart-filter-doesnt-exist": "Chytrý filtr neexistuje", - "sidenav-stream-doesnt-exist": "Stránková navigace streamu neexistuje", - "external-source-already-exists": "Externí zdroj již existuje", - "external-source-required": "Vyžaduje se ApiKey a Host", - "more-in-genre": "Více v žánru {0}", - "browse-recently-updated": "Procházet naposledy aktualizované", - "external-sources": "Externí zdroje", - "browse-external-sources": "Procházet externí zdroje", - "dashboard-stream-doesnt-exist": "Přehledová deska streamu neexistuje", - "external-source-doesnt-exist": "Externí zdroj neexistuje", - "external-source-already-in-use": "To je existující stream s tímto externím zdrojem", - "unable-to-reset-k+": "Licenci Kavita+ nelze resetovat kvůli chybě. Obraťte se na podporu Kavita+", - "browse-more-in-genre": "Procházet další v {0}", - "recently-updated": "Nedávno aktualizované", - "invalid-email": "E-mail v záznamech pro uživatele není platný e-mail. Všechny odkazy najdete v protokolech.", - "email-not-enabled": "Email není na tomto serveru aktivní. Tuto akci nelze provést.", - "license-check": "Kontrola licence", - "process-scrobbling-events": "Zpracovat Scrobbling události", - "report-stats": "Statistiky hlášení", - "check-scrobbling-tokens": "Kontrola Scrobbling tokenů", - "cleanup": "Čistka", - "process-processed-scrobbling-events": "Zpracovat zpracované Scrobbling události", - "account-email-invalid": "Email v souboru účtu správce není platný email. Nelze poslat testovací email.", - "email-settings-invalid": "Chybí informace o nastavení emailu. Ujistěte se, zda-li jsou uložena všechna nastavení.", - "send-to-unallowed": "Nemůžete odeslat do zařízení které není vaše", - "send-to-size-limit": "Soubor(y) které se snažíte poslat jsou příliš velké pro vašeho poskytovatele emailu", - "error-import-stack": "Při importu zásobníku MAL došlo k chybě", - "collection-already-exists": "Kolekce již existuje", - "generic-cover-person-save": "Nebylo možné uložit cover obrázek pro Osobu", - "generic-cover-volume-save": "Nebylo možné uložit cover obrázek pro Volume", - "check-updates": "Zkontrolovat aktualizace", - "kavita+-data-refresh": "Obnovení dat Kavita+", - "scan-libraries": "Skenovat Knihovny", - "backup": "Záloha", - "update-yearly-stats": "Aktualizovat roční statistiky", - "remove-from-want-to-read": "Vyčištění Chci si přečíst", - "person-doesnt-exist": "Osoba neexistuje", - "person-name-required": "Jméno osoby je povinné a nesmí být prázdné", - "person-name-unique": "Jméno osoby musí být unikátní", - "person-image-doesnt-exist": "Osoba neexistuje v databázi CoversDB", - "email-taken": "Email je již používán", - "kavitaplus-restricted": "Toto je omezeno pouze na Kavita+", - "dashboard-stream-only-delete-smart-filter": "Z ovládacího panelu lze odstranit pouze streamy chytrých filtrů", - "smart-filter-name-required": "Vyžaduje se název chytrého filtru", - "smart-filter-system-name": "Nelze použít název streamu poskytovaného systémem", - "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů", - "aliases-have-overlap": "Jeden nebo více aliasů se překrývají s jinými osobami, nelze je aktualizovat", - "generated-reading-profile-name": "Generováno z {0}" + "collection-deleted": "Sbírka smazána" } diff --git a/API/I18N/da.json b/API/I18N/da.json deleted file mode 100644 index 645e70303..000000000 --- a/API/I18N/da.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "locked-out": "Du er låst ude, grundet for mange forsøg. Vent venligt 10 minutter.", - "disabled-account": "Din konto er deaktiveret. Kontakt server administratoren.", - "register-user": "Der opstod en fejl i forbindelse med brugerregistreringen", - "validate-email": "Der opstod en fejl i forbindelse med validering af emailen: {0}", - "confirm-email": "Du skal bekræfte din email først", - "confirm-token-gen": "Der opstod en fejl i forbindelse med oprettelsen af en bekræftigelsestoken", - "invalid-password": "Ugyldigt kodeord", - "username-taken": "Brugernavenet er allerede taget", - "generate-token": "Der opstod en fejl under generering af bekræftigelsesemailtoken. Se logbeskederne", - "generic-user-update": "Der opstod en fejl ved opdatering af brugeren", - "user-already-registered": "Brugeren er allerede registreret som {0}", - "manual-setup-fail": "Manuel opsætning kunne ikke færdiggøres. Venligst annuller, og genopret invitationen", - "not-accessible-password": "Din server er ikke tilgængelig. Et link til nulstilling af kodeord kan findes i loggen", - "user-migration-needed": "Denne bruger skal migreres. Få dem til at logge ud og ind igen for at starte migreringsflowet", - "invalid-username": "Ugyldigt brugernavn", - "generic-invite-email": "Der opstod en fejl under et forsøg på at sende invitationmail'en igen", - "check-updates": "Tjek for opdateringer", - "update-yearly-stats": "Opdater årlige statistikker", - "no-user": "Brugeren eksistere ikke", - "user-already-confirmed": "Brugeren er allerede bekræftet", - "admin-already-exists": "Administrator eksistere allerede", - "chapter-doesnt-exist": "Kapitel findes ikke", - "not-accessible": "Din server er ikke eksternt tilgængelig", - "email-sent": "Email sendt", - "book-num": "Bog {0}", - "issue-num": "Nummer {0}{1}", - "chapter-num": "Kapitel {0}", - "backup": "Backup", - "age-restriction-update": "Der opstod en fejl ved opdateringen af aldersbegrænsningen", - "user-already-invited": "Brugeren er allerede inviteret med denne mail, og har ikke accepteret invitationen endnu.", - "invalid-email-confirmation": "Ugyldig mailbekræftigelse", - "password-updated": "Kodeord opdateret", - "denied": "Ikke tilladt", - "permission-denied": "Du har ikke tilladelse til at udføre denne operation", - "nothing-to-do": "Intet at lave", - "password-required": "Du skal skrive dit kodeord for at ændre din konto, medmindre du er en administrator", - "invalid-payload": "Ugyldig payload", - "invalid-token": "Ugyldig token", - "unable-to-reset-key": "Noget gik galt, det var ikke muligt at nulstille nøglen", - "share-multiple-emails": "Du kan ikke dele emails henover flere kontoer", - "generic-password-update": "Der opstod en uventet fejl i forbindelse med bekræftigelsen af new kodeord", - "generic-invite-user": "Der opstod et problem i forbindelse med at invitere brugeren. Tjek venligst loggen.", - "generic-user-email-update": "Det var ikke muligt at opdatere brugerens email. Tjek loggen.", - "forgot-password-generic": "En email vil blive sendt til emailen, hvis den eksistere i vores database", - "critical-email-migration": "Der opstod en fejl i forbindelse med email migration. Kontakt support", - "email-not-enabled": "Email er ikke slået til på denne server. Du har ikke mulighed for at udføre denne handling.", - "file-missing": "Filen blev ikke fundet i bogen", - "collection-updated": "Samlingen er opdateret", - "collection-deleted": "Samlingen er slettet", - "collection-doesnt-exist": "Samlingen eksistere ikke", - "generic-error": "Noget gik galt, prøv igen", - "collection-already-exists": "Samlingen eksistere allerede", - "error-import-stack": "Der opstod en fejl i forbindelse med import af MAL stack", - "device-doesnt-exist": "Enheden eksistere ikke", - "generic-device-create": "Der opstod en fejl i forbindelse med oprettelsen af enheden", - "generic-device-update": "Der opstod en fejl i forbindelse med opdatering af enheden", - "generic-device-delete": "Der opstod en fejl i forbindelse med sletningen af enheden", - "greater-0": "{0} skal være større end 0", - "generic-send-to": "Der opstod en fejl i forbindelse med at sende filerne til enheden", - "send-to-unallowed": "Du kan ikke sende til en enhed der ikke er din", - "send-to-device-status": "Overfører filer til din enhed", - "series-doesnt-exist": "Serien eksistere ikke", - "volume-doesnt-exist": "Bindet eksistere ikke", - "bookmarks-empty": "Bogmærker kan ikke være tomme", - "no-cover-image": "Intet omslagsbillede", - "file-doesnt-exist": "Filen eksistere ikke", - "library-name-exists": "Biblioteksnavnet eksistere allerede. Vælg venligst et unikt navn til serveren.", - "generic-library": "Der opstod en kritisk fejl. Prøv igen.", - "no-library-access": "Brugeren har ikke adgang til dette bibliotek", - "generic-library-update": "Der opstod en kritisk fejl i forbindelse med opdatering af biblioteket.", - "bookmark-permission": "Du har ikke tilladelse til at oprette eller fjerne bogmærker", - "name-required": "Navnet må ikke være tomt", - "valid-number": "Skal være et gyldigt sidetal", - "reading-list-permission": "Enten har du ikke tilladelse til denne læseliste, eller også eksistere listen ikke", - "reading-list-position": "Kunne ikke opdatere position", - "reading-list-item-delete": "Kunne ikke slette elementerne", - "reading-list-deleted": "Læselisten er opdateret", - "generic-reading-list-delete": "Der opstod en fejl i forbindelse med sletningen af læselisten", - "generic-reading-list-update": "Der opstod en fejl i forbindelse med opdatering af læselisten", - "generic-reading-list-create": "Der opstod en fejl i forbindelse med oprettelsen af læselisten", - "reading-list-doesnt-exist": "Læselisten eksistere ikke", - "libraries-restricted": "Brugeren har ikke adgang til nogen biblioteker", - "update-metadata-fail": "Kunne ikke opdatere metadata", - "age-restriction-not-applicable": "Ingen Restriktioner", - "job-already-running": "Opgaven kører allerede", - "ip-address-invalid": "IP-adressen '{0}' er ugyldig", - "bookmark-doesnt-exist": "Bogmærket eksistere ikke", - "must-be-defined": "{0} skal være defineret", - "library-doesnt-exist": "Biblioteket eksistere ikke", - "invalid-filename": "ugyldigt filnavn", - "invalid-path": "Ugyldig sti", - "invalid-access": "Ugyldig adgang", - "user-doesnt-exist": "Brugeren eksistere ikke", - "pdf-doesnt-exist": "PDF'en burde eksistere, men gør ikke", - "perform-scan": "Kør venligst en skanning af denne serie eller bibliotek, og prøv igen", - "bookmark-save": "Bogmærket kunne ikke gemmes", - "reading-list-updated": "Opdateret", - "series-restricted": "Brugeren har ikke adgang til denne serie", - "generic-series-delete": "Der opstod en fejl i forbindelse med sletningen af serien", - "generic-series-update": "Der opstod en fejl i forbindelse med opdateringen af serien", - "series-updated": "Succesfuldt opdateret" -} diff --git a/API/I18N/de.json b/API/I18N/de.json index a6c865897..1b42b2cc7 100644 --- a/API/I18N/de.json +++ b/API/I18N/de.json @@ -29,6 +29,7 @@ "user-already-invited": "Der Benutzer ist bereits unter dieser E-Mail eingeladen und hat die Einladung noch nicht angenommen.", "invalid-email-confirmation": "Ungültige E-Mail Bestätigung", "not-accessible": "Es kann von außen nicht auf Ihren Server zugegriffen werden", + "bad-credentials": "Ihre Anmeldedaten sind nicht korrekt", "unable-to-reset-key": "Etwas ist schief gelaufen, Schlüssel kann nicht zurückgesetzt werden", "invalid-token": "Ungültiger Token", "email-sent": "E-Mail versendet", @@ -38,10 +39,10 @@ "generic-error": "Es ist ein Fehler ist aufgetreten, bitte versuchen Sie es erneut", "device-doesnt-exist": "Das Gerät existiert nicht", "generic-device-create": "Beim Erstellen des Geräts ist ein Fehler aufgetreten", - "send-to-kavita-email": "Senden an Gerät kann ohne E-Mail-Einrichtung nicht verwendet werden", + "send-to-kavita-email": "Das Senden an Gerät kann nicht mit dem E-Mail-Dienst von Kavita durchgeführt werden. Bitte konfigurieren Sie Ihren eigenen.", "send-to-device-status": "Übertrage Dateien auf Ihr Gerät", "series-doesnt-exist": "Die Serie existiert nicht", - "volume-doesnt-exist": "Der Band existiert nicht", + "volume-doesnt-exist": "Das Band existiert nicht", "no-cover-image": "Kein Coverbild", "bookmark-doesnt-exist": "Lesezeichen ist nicht vorhanden", "must-be-defined": "{0} muss definiert sein", @@ -111,7 +112,7 @@ "user-no-access-library-from-series": "Der Benutzer hat keinen Zugang zu der Bibliothek, der zu dieser Serie gehört", "series-restricted-age-restriction": "Benutzer darf diese Serie aufgrund von Altersbeschränkungen nicht sehen", "book-num": "Buch {0}", - "issue-num": "Ausgabe {0}{1}", + "issue-num": "Fehler {0}{1}", "chapter-num": "Kapitel {0}", "reading-list-position": "Position konnte nicht aktualisiert werden", "libraries-restricted": "Benutzer hat keinen Zugriff auf jegliche Bibliothek", @@ -170,44 +171,5 @@ "external-source-doesnt-exist": "Externe Quelle existiert nicht", "external-sources": "Externe Quellen", "external-source-required": "ApiSchlüssel und Host erforderlich", - "smart-filter-already-in-use": "Es gibt einen bestehenden Stream mit diesem Smart Filter", - "more-in-genre": "Mehr in Genre {0}", - "browse-more-in-genre": "Mehr in {0} stöbern", - "recently-updated": "Zuletzt aktualisiert", - "browse-recently-updated": "Zuletzt aktualisiert durchsuchen", - "unable-to-reset-k+": "Aufgrund eines Fehlers konnte die Kavita+ Lizenz nicht zurückgesetzt werden. Kontaktieren Sie den Kavita+ Support", - "email-not-enabled": "Der Mailversand ist auf diesem Server nicht aktiviert. Sie können diese Aktion nicht durchführen.", - "invalid-email": "Die für den Benutzer hinterlegte E-Mail ist ungültig. Links finden Sie in den Logs.", - "send-to-unallowed": "Sie können nicht an ein Gerät senden, das nicht Ihnen gehört", - "send-to-size-limit": "Die Datei(en), die Sie zu senden versuchen, sind zu groß für Ihren E-Mail-Anbieter", - "check-updates": "Updates überprüfen", - "email-settings-invalid": "E-Mail-Einstellungen fehlen Informationen. Stellen Sie sicher, dass alle E-Mail-Einstellungen gespeichert sind.", - "account-email-invalid": "Die für das Admin-Konto gespeicherte E-Mail ist nicht gültig. Test-E-Mail kann nicht gesendet werden.", - "license-check": "Lizenzprüfung", - "process-scrobbling-events": "Verarbeite Scrobble-Events", - "report-stats": "Statistiken melden", - "check-scrobbling-tokens": "Überprüfe die Scrobbling-Tokens", - "cleanup": "Bereinigung", - "process-processed-scrobbling-events": "Verarbeitete Scrobbling Ereignisse", - "remove-from-want-to-read": "Möchte Lesen Liste Bereinigung", - "kavita+-data-refresh": "Kavita+ Daten Aktualisierung", - "backup": "Sichern", - "update-yearly-stats": "Aktualisiere Jahresstatistiken", - "error-import-stack": "Es gab Problem beim Importieren des MAL stack", - "scan-libraries": "Bibliotheken scannen", - "collection-already-exists": "Sammlung existiert schon", - "generic-cover-volume-save": "Coverbild kann nicht auf Volume gespeichert werden", - "generic-cover-person-save": "Das Titelbild kann nicht zu dieser Person gespeichert werden", - "person-doesnt-exist": "Die Person existiert nicht", - "person-name-required": "Personenname ist erforderlich und darf nicht null sein", - "person-name-unique": "Der Name der Person muss eindeutig sein", - "person-image-doesnt-exist": "Die Person existiert nicht in CoversDB", - "email-taken": "E-Mail bereits in Gebrauch", - "kavitaplus-restricted": "Dies ist nur auf Kavita+ beschränkt", - "sidenav-stream-only-delete-smart-filter": "Nur Smart-Filter-Streams können aus der Seitennavigation gelöscht werden", - "dashboard-stream-only-delete-smart-filter": "Nur Smart-Filter-Streams können aus dem Dashboard gelöscht werden", - "smart-filter-system-name": "Du kannst den Namen eines vom System bereitgestellten Streams nicht verwenden", - "smart-filter-name-required": "Name des Smart Filters erforderlich", - "aliases-have-overlap": "Ein oder mehrere Aliasnamen sind mit anderen Personen identisch und können nicht aktualisiert werden", - "generated-reading-profile-name": "Erstellt aus {0}" + "smart-filter-already-in-use": "Es gibt einen bestehenden Stream mit diesem Smart Filter" } diff --git a/API/I18N/el.json b/API/I18N/el.json deleted file mode 100644 index 7a1173662..000000000 --- a/API/I18N/el.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "confirm-email": "Πρέπει να επιβεβαιώσετε το email σας πρώτα", - "disabled-account": "Ο λογαριασμός σας είναι απενεργοποιημένος. Επικοινωνήστε με τον διαχειριστή του διακομιστή.", - "permission-denied": "Δεν επιτρέπεται αυτή η λειτουργία", - "invalid-password": "Άκυρος κωδικός πρόσβασης", - "invalid-token": "Άκυρο token", - "unable-to-reset-key": "Κάτι πήγε λάθος, αδυναμία επαναφοράς του κλειδιού", - "invalid-payload": "Άκυρο payload", - "nothing-to-do": "Τίποτα να κάνετε", - "share-multiple-emails": "Δεν μπορείτε να μοιράζεστε πολλαπλά email σε πολλαπλούς λογαριασμούς", - "generate-token": "Υπήρξε ένα πρόβλημα δημιουργώντας ένα token επιβεβαίωσης email. Δείτε τα αρχεία καταγραφής", - "no-user": "Ο χρήστης δεν υπάρχει", - "username-taken": "Το ψευδώνυμο είναι κατειλημμένο", - "generic-user-update": "Υπήρξε μια εξαίρεση κατά την ενημέρωση του χρήστη", - "user-already-registered": "Ο χρήστης είναι ήδη εγγεγραμμένος ως {0}", - "chapter-doesnt-exist": "Το κεφάλαιο δεν υπάρχει", - "file-missing": "Το αρχείο δεν βρέθηκε στο βιβλίο", - "send-to-unallowed": "Δεν μπορείτε να στείλετε σε μία συσκευή που δεν είναι δική σας", - "generic-favicon": "Υπήρξε ένα πρόβλημα με το favicon για το domain", - "invalid-filename": "Άκυρο Όνομα Αρχείου", - "no-cover-image": "Δεν υπάρχει εικόνα εξωφύλλου", - "generic-password-update": "Υπήρξε ένα απροσδόκητο σφάλμα κατά την επιβεβαίωση του νέου κωδικού πρόσβασης", - "locked-out": "Έχετε αποκλειστεί από πάρα πολλές προσπάθειες εξουσιοδότησης. Παρακαλώ περιμένετε 10 λεπτά.", - "validate-email": "Υπήρξε ένα πρόβλημα επιβεβαιώνοντας το email σας: {0}", - "password-required": "Πρέπει να εισάγετε τον υπάρχοντα κωδικό πρόσβασης σας για να αλλάξετε το λογαριασμό σας, εκτός αν είστε διαχειριστής", - "register-user": "Κάτι πήγε λάθος κατά την εγγραφή του χρήστη", - "confirm-token-gen": "Υπήρξε ένα πρόβλημα με τη δημιουργίας ενός token επιβεβαίωσης", - "denied": "Δεν επιτρέπεται", - "age-restriction-update": "Υπήρξε ένα πρόβλημα ενημερώνοντας τον περιορισμό ηλικίας", - "user-already-confirmed": "Ο χρήστης έχει ήδη επιβεβαιωθεί", - "manual-setup-fail": "Η χειροκίνητη ρύθμιση δεν μπόρεσε να ολοκληρωθεί. Παρακαλούμε ακυρώστε και δημιουργήστε ξανά την πρόσκληση", - "invalid-email-confirmation": "Άκυρο email επιβεβαίωσης", - "user-already-invited": "Ο χρήστης έχει ήδη προσκληθεί με αυτό το email και δεν έχει αποδεχτεί ακόμη την πρόσκληση.", - "generic-invite-user": "Υπήρξε ένα πρόβλημα με την πρόσκληση του χρήστη. Ελέγξτε τα αρχεία καταγραφής.", - "generic-user-email-update": "Αδυναμία ενημέρωσης του email του χρήστη. Ελέγξτε τα αρχεία καταγραφής.", - "forgot-password-generic": "Θα σας σταλθεί ένα email αν υπάρχει στη βάση δεδομένων μας", - "password-updated": "Ενημερωμένος κωδικός πρόσβασης", - "not-accessible-password": "Ο διακομιστής σας δεν είναι προσβάσιμος. Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης σας βρίσκεται στα αρχεία καταγραφής", - "invalid-email": "Το email στο αρχείο του χρήστη δεν είναι έγκυρο. Δείτε τα αρχεία καταγραφής για τυχόν συνδέσμους.", - "not-accessible": "Ο διακομιστής σας δεν είναι προσβάσιμος εξωτερικά", - "email-sent": "Στάλθηκε email", - "user-migration-needed": "Αυτός ο χρήστης πρέπει να μεταβιβαστεί. Βάλτε τον να αποσυνδεθεί και να συνδεθεί για να ενεργοποιήσετε μια ροή μετάβασης.", - "invalid-username": "Άκυρο ψευδώνυμο", - "critical-email-migration": "Υπήρξε ένα λάθος κατά τη διάρκεια της μετάβασης email. Επικοινωνήστε με την υποστήριξη", - "email-not-enabled": "Το email δεν είναι ενεργοποιημένο σε αυτόν τον διακομιστή. Δεν μπορείτε να εκτελέσετε αυτή την ενέργεια.", - "generic-invite-email": "Υπήρξε πρόβλημα με την επαναποστολή του email πρόσκλησης", - "admin-already-exists": "Υπάρχει ήδη διαχειριστής", - "account-email-invalid": "Το email στο αρχείο του λογαριασμού διαχειριστή δεν είναι έγκυρο. Δεν είναι δυνατή η αποστολή δοκιμαστικού email.", - "email-settings-invalid": "Λείπουν πληροφορίες από τις ρυθμίσεις email. Βεβαιωθείτε ότι έχουν αποθηκευτεί όλες οι ρυθμίσεις email.", - "collection-updated": "Η συλλογή ενημερώθηκε επιτυχώς", - "generic-error": "Κάτι πήγε στραβά, δοκιμάστε ξανά", - "collection-deleted": "Η συλλογή διαγράφτηκε", - "collection-doesnt-exist": "Η συλλογή δεν υπάρχει", - "collection-already-exists": "Η συλλογή υπάρχει ήδη", - "error-import-stack": "Υπήρξε ένα πρόβλημα εισαγωγής στοίβας MAL", - "generic-device-update": "Υπήρξε ένα πρόβλημα στην ενημέρωση της συσκευής", - "device-doesnt-exist": "Η συσκευή δεν υπάρχει", - "generic-device-create": "Υπήρξε ένα πρόβλημα στην δημιουργία της συσκευής", - "generic-device-delete": "Υπήρξε ένα πρόβλημα στην διαγραφή της συσκευής", - "greater-0": "{0} πρέπει να είναι μεγαλύτερο από το 0", - "send-to-kavita-email": "Η αποστολή στη συσκευή δεν μπορεί να χρησιμοποιηθεί χωρίς τη ρύθμιση Email", - "send-to-size-limit": "Το(α) αρχείο(α) που προσπαθείτε να στείλετε είναι πολύ μεγάλο(α) για τον emailer σας", - "generic-send-to": "Υπήρξε ένα πρόβλημα κατά την αποστολή του(ων) αρχείου(ων) στην συσκευή", - "send-to-device-status": "Μεταφέρονται αρχεία στην συσκευή σας", - "series-doesnt-exist": "Η σειρά δεν υπάρχει", - "volume-doesnt-exist": "Ο τόμος δεν υπάρχει", - "bookmarks-empty": "Οι σελιδοδείκτες δεν μπορούν να είναι άδειοι", - "bookmark-doesnt-exist": "Δεν υπάρχει ο σελιδοδείκτης", - "must-be-defined": "{0} πρέπει να οριστεί", - "file-doesnt-exist": "Το αρχείο δεν υπάρχει" -} diff --git a/API/I18N/en.json b/API/I18N/en.json index d3cd1ecd3..861bebae1 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -1,5 +1,6 @@ { "confirm-email": "You must confirm your email first", + "bad-credentials": "Your credentials are not correct", "locked-out": "You've been locked out from too many authorization attempts. Please wait 10 minutes.", "disabled-account": "Your account is disabled. Contact the server admin.", "register-user": "Something went wrong when registering user", @@ -18,7 +19,6 @@ "age-restriction-update": "There was an error updating the age restriction", "no-user": "User does not exist", "username-taken": "Username already taken", - "email-taken": "Email already in use", "user-already-confirmed": "User is already confirmed", "generic-user-update": "There was an exception when updating the user", "manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite", @@ -39,9 +39,6 @@ "admin-already-exists": "Admin already exists", "invalid-username": "Invalid username", "critical-email-migration": "There was an issue during email migration. Contact support", - "email-not-enabled": "Email is not enabled on this server. You cannot perform this action.", - "account-email-invalid": "The email on file for the admin account is not a valid email. Cannot send test email.", - "email-settings-invalid": "Email settings missing information. Ensure all email settings are saved.", "chapter-doesnt-exist": "Chapter does not exist", "file-missing": "File was not found in book", @@ -50,22 +47,13 @@ "collection-deleted": "Collection deleted", "generic-error": "Something went wrong, please try again", "collection-doesnt-exist": "Collection does not exist", - "collection-already-exists":"Collection already exists", - "error-import-stack": "There was an issue importing MAL stack", - - "person-doesnt-exist": "Person does not exist", - "person-name-required": "Person name is required and must not be null", - "person-name-unique": "Person name must be unique", - "person-image-doesnt-exist": "Person does not exist in CoversDB", "device-doesnt-exist": "Device does not exist", "generic-device-create": "There was an error when creating the device", "generic-device-update": "There was an error when updating the device", "generic-device-delete": "There was an error when deleting the device", "greater-0": "{0} must be greater than 0", - "send-to-kavita-email": "Send to device cannot be used without Email setup", - "send-to-unallowed":"You cannot send to a device that isn't yours", - "send-to-size-limit": "The file(s) you are trying to send are too large for your email provider", + "send-to-kavita-email": "Send to device cannot be used with Kavita's email service. Please configure your own.", "send-to-device-status": "Transferring files to your device", "generic-send-to": "There was an error sending the file(s) to the device", "series-doesnt-exist": "Series does not exist", @@ -144,8 +132,6 @@ "generic-cover-reading-list-save": "Unable to save cover image to Reading List", "generic-cover-chapter-save": "Unable to save cover image to Chapter", "generic-cover-library-save": "Unable to save cover image to Library", - "generic-cover-person-save": "Unable to save cover image to Person", - "generic-cover-volume-save": "Unable to save cover image to Volume", "access-denied": "You do not have access", "reset-chapter-lock": "Unable to resetting cover lock for Chapter", @@ -165,10 +151,6 @@ "browse-libraries": "Browse by Libraries", "collections": "All Collections", "browse-collections": "Browse by Collections", - "more-in-genre": "More in Genre {0}", - "browse-more-in-genre": "Browse more in {0}", - "recently-updated": "Recently Updated", - "browse-recently-updated": "Browse Recently Updated", "smart-filters": "Smart Filters", "external-sources": "External Sources", "browse-external-sources": "Browse External Sources", @@ -186,14 +168,9 @@ "external-source-required": "ApiKey and Host required", "external-source-doesnt-exist": "External Source doesn't exist", "external-source-already-in-use": "There is an existing stream with this External Source", - "sidenav-stream-only-delete-smart-filter": "Only smart filter streams can be deleted from the SideNav", - "dashboard-stream-only-delete-smart-filter": "Only smart filter streams can be deleted from the dashboard", - "smart-filter-name-required": "Smart Filter name required", - "smart-filter-system-name": "You cannot use the name of a system provided stream", "not-authenticated": "User is not authenticated", "unable-to-register-k+": "Unable to register license due to error. Reach out to Kavita+ Support", - "unable-to-reset-k+": "Unable to reset Kavita+ license due to error. Reach out to Kavita+ Support", "anilist-cred-expired": "AniList Credentials have expired or not set", "scrobble-bad-payload": "Bad payload from Scrobble Provider", "theme-doesnt-exist": "Theme file missing or invalid", @@ -211,27 +188,14 @@ "reading-list-name-exists": "A reading list of this name already exists", "user-no-access-library-from-series": "User does not have access to the library this series belongs to", "series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions", - "kavitaplus-restricted": "This is restricted to Kavita+ only", - "aliases-have-overlap": "One or more of the aliases have overlap with other people, cannot update", + + "volume-num": "Volume {0}", "book-num": "Book {0}", "issue-num": "Issue {0}{1}", - "chapter-num": "Chapter {0}", + "chapter-num": "Chapter {0}" - "check-updates": "Check Updates", - "license-check": "License Check", - "process-scrobbling-events": "Process Scrobbling Events", - "report-stats": "Report Stats", - "check-scrobbling-tokens": "Check Scrobbling Tokens", - "cleanup": "Cleanup", - "process-processed-scrobbling-events": "Process Processed Scrobbling Events", - "remove-from-want-to-read": "Want to Read Cleanup", - "scan-libraries": "Scan Libraries", - "kavita+-data-refresh": "Kavita+ Data Refresh", - "backup": "Backup", - "update-yearly-stats": "Update Yearly Stats", - "generated-reading-profile-name": "Generated from {0}" } diff --git a/API/I18N/es.json b/API/I18N/es.json index ca1a5c38a..55c5842cd 100644 --- a/API/I18N/es.json +++ b/API/I18N/es.json @@ -1,4 +1,5 @@ { + "bad-credentials": "Las credenciales son incorrectas", "confirm-email": "Debes confirmar el correo electrónico primero", "disabled-account": "La cuenta está deshabilitada. Contacta con un administrador.", "validate-email": "Ha habido un error al validar el correo: {0}", @@ -22,7 +23,7 @@ "bookmarks-empty": "Los marcadores no pueden estar vacíos", "must-be-defined": "{0} debe estar definido", "invalid-filename": "Nombre de archivo no válido", - "library-name-exists": "El nombre de la biblioteca ya existe. Elija un nombre unívoco para el servidor.", + "library-name-exists": "El nombre de la biblioteca ya existe. Por favor, elige un nombre único.", "user-doesnt-exist": "El usuario no existe", "library-doesnt-exist": "La biblioteca no existe", "age-restriction-update": "Ha ocurrido un error al actualizar la restricción de edad", @@ -37,12 +38,12 @@ "forgot-password-generic": "Se enviará el correo si la dirección existe en nuestra base de datos", "generic-device-create": "Ha ocurrido un error al crear el dispositivo", "greater-0": "{0} debe ser mayor que 0", - "send-to-kavita-email": "Enviar al dispositivo no se puede utilizar sin configurar el correo electrónico", - "no-cover-image": "No hay imagen de cubierta", + "send-to-kavita-email": "Enviar al dispositivo no se puede utilizar con el servicio de correo electrónico de Kavita. Por favor, configura el tuyo propio.", + "no-cover-image": "No hay imagen de portada", "bookmark-doesnt-exist": "El marcador no existe", "generic-favicon": "Ha ocurrido un error al obtener el icono para el dominio", "file-doesnt-exist": "El archivo no existe", - "generic-library": "Ha ocurrido un error grave. Inténtelo de nuevo.", + "generic-library": "Ha ocurrido un error fatal. Por favor, inténtalo de nuevo.", "no-library-access": "El usuario no tiene acceso a esta biblioteca", "no-user": "El usuario no existe", "username-taken": "El nombre de usuario ya existe", @@ -87,7 +88,7 @@ "update-metadata-fail": "No se han podido actualizar los metadatos", "generic-relationship": "Hubo un problema al actualizar las relaciones", "job-already-running": "Trabajo ya en ejecución", - "ip-address-invalid": "La dirección IP «{0}» no es válida", + "ip-address-invalid": "La dirección IP '{0}' no es válida", "bookmark-dir-permissions": "El directorio de marcadores no tiene los permisos correctos para que Kavita pueda utilizarlo", "total-backups": "El número total de copias de seguridad debe estar entre 1 y 30", "stats-permission-denied": "No está autorizado a ver las estadísticas de otro usuario", @@ -116,18 +117,18 @@ "generic-create-temp-archive": "Hubo un problema al crear un archivo temporal", "epub-malformed": "¡El archivo está malformado! No se puede leer.", "book-num": "Libro {0}", - "issue-num": "Número {0}{1}", + "issue-num": "Incidencia {0}{1}", "search-description": "Buscar series, colecciones o listas de lectura", "unable-to-register-k+": "No se ha podido registrar la licencia debido a un error. Póngase en contacto con el servicio de asistencia de Kavita", "bad-copy-files-for-download": "No se pueden copiar archivos al directorio temporal de descarga de archivos.", - "send-to-permission": "No se pueden enviar archivos que no sean EPUB o PDF a los dispositivos porque Kindle no los admite", + "send-to-permission": "No se puede enviar archivos que no sean EPUB o PDF a dispositivos no compatibles con Kindle", "progress-must-exist": "El progreso debe existir en el usuario", "epub-html-missing": "No se ha podido encontrar el HTML apropiado para esa página", "collection-tag-duplicate": "Ya existe una colección con este nombre", "device-duplicate": "Ya existe un dispositivo con este nombre", "collection-tag-title-required": "El título de la colección no puede estar vacío", "reading-list-title-required": "El título de la lista de lectura no puede estar vacío", - "device-not-created": "Este dispositivo aún no existe. Créelo primero", + "device-not-created": "Este dispositivo aún no existe. Por favor, créelo primero", "reading-list-name-exists": "Ya existe una lista de lectura con este nombre", "user-no-access-library-from-series": "El usuario no tiene acceso a la biblioteca a la que pertenece esta serie", "series-restricted-age-restriction": "El usuario no puede ver esta serie debido a restricciones de edad", @@ -169,39 +170,6 @@ "sidenav-stream-doesnt-exist": "SideNav Stream no existe", "external-source-doesnt-exist": "La fuente externa no existe", "external-sources": "Fuentes externas", - "external-source-required": "Se requiere la clave de API y el anfitrión", - "smart-filter-already-in-use": "Existe una transmisión con este filtro inteligente", - "invalid-email": "La dirección de correo electrónico del usuario no es válida. Consulte los registros para ver si hay algún enlace.", - "browse-more-in-genre": "Ver más en {0}", - "more-in-genre": "Más en el género {0}", - "recently-updated": "Actualizado recientemente", - "browse-recently-updated": "Examinar las últimas actualizaciones", - "unable-to-reset-k+": "No se ha podido restablecer la licencia de Kavita+ debido a un error. Contacta con el soporte de Kavita", - "send-to-unallowed": "No puede enviar a un dispositivo que no sea el suyo", - "email-not-enabled": "El correo electrónico no está habilitado en este servidor. No puede realizar esta acción.", - "send-to-size-limit": "El(Los) archivo(s) que intenta enviar es(son) demasiado(s) grande(s) para su proveedor de correo electrónico", - "process-scrobbling-events": "Procesar eventos de scrobbling", - "report-stats": "Informe de estadísticas", - "check-scrobbling-tokens": "Comprobar los token de scrobbling", - "process-processed-scrobbling-events": "Volver a procesar eventos de scrobbling procesados", - "cleanup": "Limpieza", - "remove-from-want-to-read": "Eliminar de querer leer", - "kavita+-data-refresh": "Actualización de los datos de Kavita+", - "backup": "Copia de respaldo", - "update-yearly-stats": "Actualizar estadísticas anualmente", - "license-check": "Comprobar la licencia", - "scan-libraries": "Escanear la biblioteca", - "check-updates": "Comprobar actualizaciones", - "account-email-invalid": "El correo electrónico registrado para la cuenta de administrador no es un correo electrónico válido. No se puede enviar un correo electrónico de prueba.", - "email-settings-invalid": "Falta información en la configuración de correo electrónico. Asegúrese de que todas las configuraciones de correo electrónico estén guardadas.", - "collection-already-exists": "La colección ya existe", - "error-import-stack": "Problema al importar la pila MAL", - "generic-cover-person-save": "No se puede guardar la imagen de portada para esta persona", - "generic-cover-volume-save": "No se puede guardar la imagen de portada en el volumen", - "person-doesnt-exist": "No existe la persona", - "person-name-required": "El nombre de la persona es obligatorio y no debe estar vacío", - "person-name-unique": "El nombre de la persona debe ser único", - "person-image-doesnt-exist": "La persona no existe en CoversDB", - "email-taken": "El correo electrónico ya está en uso", - "kavitaplus-restricted": "Esto está restringido a Kavita+" + "external-source-required": "Se requiere la clave API y el host", + "smart-filter-already-in-use": "Existe una transmisión con este filtro inteligente" } diff --git a/API/I18N/et.json b/API/I18N/et.json deleted file mode 100644 index b1783e987..000000000 --- a/API/I18N/et.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "confirm-email": "Esmalt pead oma e-posti kinnitama", - "locked-out": "Sinu konto on liiga paljude ebaõnnestunud sisselogimiskatsete tõttu süsteemis piiratud. Palun oota 10 minutit.", - "disabled-account": "Su konto on keelatud. Võta ühendust serveri administraatoriga.", - "register-user": "Kasutaja registreerimisel läks midagi valesti", - "validate-email": "Teie e-posti kinnitamisel ilmnes probleem: {0}", - "denied": "Pole lubatud", - "permission-denied": "Sul ei ole selle toimingu jaoks luba", - "password-required": "Kui sa pole administraator, pead oma konto muutmiseks sisestama olemasoleva parooli", - "invalid-password": "Vale Parool", - "invalid-token": "Vale kinnituskood", - "unable-to-reset-key": "Midagi läks valesti, võtit ei saa lähtestada", - "invalid-payload": "Vigane saadetis", - "share-multiple-emails": "E-maili ei saa erinevate kontode vahel jagada", - "generate-token": "Kinnitusmeili koodi loomisel ilmnes probleem. Vaata logisid", - "confirm-token-gen": "Kinnituskoodi loomisel ilmnes probleem", - "nothing-to-do": "Pole midagi teha", - "age-restriction-update": "Vanusepiirangu värskendamisel ilmnes viga", - "manual-setup-fail": "Mitteautomaatne seadistus ei suuda lõpetada. Palun katkestage ja looge kutse uuesti", - "user-already-registered": "Kasutaja on juba registreeritud kui {0}", - "user-already-confirmed": "Kasutaja on juba kinnitatud", - "generic-user-update": "Kasutajaandmete uuendamisel tekkis viga", - "user-already-invited": "Selle e-posti aadressiga kasutaja on juba kutsutud ja kutse vajab jaatavalt vastamist.", - "generic-invite-user": "Tekkis probleem kasutaja kutsumisel. Palun loe vealogisid.", - "no-user": "Sellenimelist kasutajat ei ole", - "username-taken": "Kasutajanimi on juba kasutuses", - "send-to-unallowed": "Sa ei saa saata seadmele, mis ei ole sinu", - "generic-send-to": "Tekkis viga faili(de) seadmele saatmisel", - "bookmarks-empty": "Järjehoidjad ei saa olla tühjad", - "no-cover-image": "Pole kaanepilti", - "bookmark-doesnt-exist": "Järjehoidjat ei eksisteeri", - "must-be-defined": "{0} peab olema sätestatud", - "generic-favicon": "Domeeni favicon laadimisel tekkis probleem", - "generic-library": "Tekkis möödapääsmatu probleem. Palun proovi uuesti.", - "series-updated": "Edukalt uuendatud", - "no-library-access": "Kasutaja ei oma juurdepääsu sellele kogule", - "user-doesnt-exist": "Kasutajat ei eksisteeri", - "delete-library-while-scan": "Ei saa kustutada tervikkogu, kui skaneerimine on töös. Oota skaneerimise lõppemist või taaskäivita Kavita ning püüa siis uuesti kustutada", - "valid-number": "Peab olema pädev leheküljenumber", - "generic-reading-list-update": "Lugemisloendi uuendamisel tekkis probleem", - "reading-list-position": "Ei õnnestunud uuendada järge", - "series-restricted": "Kasutajal puudub ligipääs sellele sarjale", - "update-yearly-stats": "Uuenda aastate kaupa statistika", - "remove-from-want-to-read": "Lugemisloendi puhastus", - "process-scrobbling-events": "Töötle scrobble juhtumeid", - "user-no-access-library-from-series": "Kasutaja ei oma juurdepääsu täiskogule, milles see seeria on", - "progress-must-exist": "Kasutajal peab olema järg", - "generic-create-temp-archive": "Tekkis probleem ajutise arhiivi loomisel", - "smart-filter-doesnt-exist": "Nutikas filter ei eksisteeri", - "browse-external-sources": "Lehitse väliseid allikaid", - "recently-updated": "Hiljuti uuendatud", - "browse-recently-updated": "Lehitse hiljuti uuendatuid", - "collections": "Kõik kogumid", - "browse-libraries": "Lehitse täiskogude kaupa", - "password-updated": "Parool uuendatud", - "invalid-username": "Vigane kasutajanimi", - "critical-email-migration": "E-posti migreerimisel tekkis viga. Võta toega ühendust", - "email-not-enabled": "E-post ei ole sellel serveril seadistatud. Seda muudatust ei saa teha.", - "chapter-doesnt-exist": "Peatükki ei ole olemas", - "file-missing": "Faili ei leitud raamatust", - "collection-updated": "Kogu uuendatud edukalt", - "collection-deleted": "Kogu kustutatud", - "generic-error": "Midagi ebaõnnestus, palun proovi uuesti", - "collection-doesnt-exist": "Kogu ei eksisteeri", - "device-doesnt-exist": "Seade ei eksisteeri", - "greater-0": "{0} peab olema suurem, kui 0", - "send-to-kavita-email": "Seadmele saatmine ei saa töötada ilma e-posti seadistamata", - "series-doesnt-exist": "Sari ei eksisteeri", - "file-doesnt-exist": "Faili ei eksisteeri", - "library-name-exists": "Kogu nimi juba eksisteerib, palun vali süsteemisiseselt unikaalne nimi.", - "generic-reading-list-create": "Lugemisloendi loomisel tekkis probleem", - "reading-list-doesnt-exist": "Lugemisloendit ei eksisteeri", - "browse-reading-lists": "Lehitse lugemisloendite kaupa", - "external-sources": "Välised allikad", - "smart-filters": "Nutikad filtrid", - "search": "Otsing", - "query-required": "Päringuparameeter on vaja kaasa anda", - "external-source-doesnt-exist": "Väline allikas ei eksisteeri", - "external-source-required": "APIvõti ja serverinimi on vajalikud", - "external-source-already-exists": "Väline allikas on juba olemas", - "device-duplicate": "Sellenimeline seade juba eksisteerib", - "collection-tag-duplicate": "Sellenimeline kogu juba eksisteerib", - "chapter-num": "Peatükk {0}", - "license-check": "Litsentsikontroll", - "check-updates": "Kontrolli uuendusi", - "process-processed-scrobbling-events": "Töötle juba töödeldud scrobble juhtumid", - "backup": "Varund", - "collection-already-exists": "Kogu juba eksisteerib", - "error-import-stack": "Tekkis probleem MAL kuhja importimisel", - "send-to-device-status": "Edastame failid sinu seadmele", - "volume-doesnt-exist": "Raamat ei eksisteeri", - "invalid-filename": "Vigane failinimi", - "generic-library-update": "Tekkis möödapääsmatu probleem tervikkogu uuendamisel.", - "pdf-doesnt-exist": "PDF ei eksisteeri - samas peaks", - "cache-file-find": "Ei leidnud puhverdatud pilti - taaslae ja proovi uuesti.", - "name-required": "Nimi ei või tühjaks jääda", - "reading-list-permission": "Teil ei ole õigusi sellele lugemisloendile või seda loendit ei eksisteeri", - "reading-list-item-delete": "Ei õnnestunud kustutada element(e|i)", - "reading-list-deleted": "Lugemisloend on kustutatud", - "generic-reading-list-delete": "Lugemisloendi kustutamisel tekkis probleem", - "browse-recently-added": "Lehitse hiljuti lisatuid", - "cleanup": "Puhastus", - "issue-num": "Väljaanne {0}{1}", - "book-num": "Raamat {0}", - "series-restricted-age-restriction": "Kasutajal ei ole vanusepiirangust tulenevalt seeriale juurdepääsu", - "send-to-permission": "Kindle ei toeta mitte-EPUB või mitte-PDF formaati", - "device-not-created": "See seade veel ei eksisteeri. Palun loo seade", - "collection-tag-title-required": "Kogu pealkiri ei saa jääda tühjaks", - "epub-html-missing": "Ei suutnud leida sobivat HTML selle lehekülje jaoks", - "epub-malformed": "Failis on süntaksivead! Ei saa lugeda.", - "theme-doesnt-exist": "Teemafail puudub või on vigane", - "external-source-already-in-use": "Juba eksisteerib voog selle välise allikaga", - "sidenav-stream-doesnt-exist": "SideNav voog ei eksisteeri", - "dashboard-stream-doesnt-exist": "Koondpaneeli voog ei eksisteeri", - "smart-filter-already-in-use": "Juba eksisteerib selle nutika filtriga voog", - "favicon-doesnt-exist": "Faviconi ei eksisteeri", - "search-description": "Otsi sarju, kogusid või lugemisloendeid", - "reading-list-restricted": "Lugemisloend ei eksisteeri või teil puudub juurdepääs", - "browse-smart-filters": "Lehitse nutikate filtritega", - "browse-more-in-genre": "Brausi rohkem {0}", - "more-in-genre": "Rohkem žanris {0}", - "browse-collections": "Lehitse kogude kaupa", - "reading-lists": "Lugemisloendid", - "invalid-email-confirmation": "Vigane e-posti kinnitus", - "generic-user-email-update": "Pole võimalik uuendada kasutaja e-posti aadressi. Kontrolli logisid.", - "not-accessible": "Sinu server ei ole väljast ligipääsetav", - "invalid-email": "E-posti aadress selle kasutaja juures ei vasta RFC-le. Vaata palun logisid.", - "not-accessible-password": "Sinu server ei ole ligipääsetav. Sinu parooli taasseadmise link on logides", - "forgot-password-generic": "E-post saadetakse aadressile, mis on meie andmebaasis", - "generic-password-update": "Uue parooli kinnitamisel esines ootamatu viga", - "email-sent": "E-post saadetud", - "user-migration-needed": "Selle kasutaja andmeid on vaja migreerida. Palu tal välja logida, et saaks migratsiooni töövoo käivitada", - "generic-invite-email": "Esines probleem e-postiga kutse taas-saatmisel", - "admin-already-exists": "Administraator on juba määratud", - "account-email-invalid": "Selle administraatorkonto e-posti aadress ei vasta RFC-le. Test e-posti ei saa saata.", - "email-settings-invalid": "E-posti seaded on puudulikud. Veendu, et e-posti seaded saaksid salvestatud.", - "generic-device-create": "Tekkis viga seadme loomisel", - "generic-device-update": "Tekkis viga seadme uuendamisel", - "generic-device-delete": "Tekkis viga seadme kustutamisel", - "send-to-size-limit": "Fail(id) mida üritad saata on e-posti serveri jaoks liiga suured", - "library-doesnt-exist": "Tervikkogu ei eksisteeri", - "invalid-access": "Vigane juurdepääs", - "no-image-for-page": "Pole sellist pilti leheküljel {0}. Proovi taaslaadimist, et võimaldada taaspuhverdamine.", - "bookmark-save": "Ei õnnestunud salvestada järjehoidjat", - "perform-scan": "Palun viige läbi selle seeria või täiskogu skaneerimine, ning proovige uuesti", - "generic-read-progress": "Tekkis probleem järje salvestamisel", - "generic-clear-bookmarks": "Ei suutnud puhastada järjehoidjaid", - "bookmark-permission": "Teil ei ole õigust järjehoidja seadmiseks/kustutamiseks", - "duplicate-bookmark": "Järjehoidja topeltsissekanne on juba olemas", - "reading-list-updated": "Uuendatud", - "bad-copy-files-for-download": "Ei õnnestu kopeerida faile ajutisse kataloogi arhiivina allalaadimiseks.", - "volume-num": "Köide {0}", - "reading-list-title-required": "Lugemisloendi pealkiri ei saa jääda tühjaks", - "reading-list-name-exists": "Sellenimeline lugemisloend on juba olemas", - "check-scrobbling-tokens": "Kontrolli scrobble turvažetoone", - "report-stats": "Raporteeri statistika", - "invalid-path": "Vigane tee", - "not-authenticated": "Kasutaja on autentimata", - "kavita+-data-refresh": "Kavita+andmete värskendus", - "scan-libraries": "Skaneeri täiskogud" -} diff --git a/API/I18N/fa.json b/API/I18N/fa.json deleted file mode 100644 index 0933c41d5..000000000 --- a/API/I18N/fa.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "validate-email": "در اعتبارسنجی ایمیل شما مشکلی رخ داد: {0}", - "confirm-email": "اول ایمیل خود را تایید کنید", - "locked-out": "شما به علت تلاش‌های ورود بیش از حد، محدود شدید. لطفاً ۱۰ دقیقه صبر کنید.", - "disabled-account": "حساب شما غیرفعال شده است ، لطفاً با مدیر سرور تماس حاصل کنید.", - "register-user": "در هنگام ثبت کاربر مشکلی رخ داد." -} diff --git a/API/I18N/fi.json b/API/I18N/fi.json deleted file mode 100644 index dd2a3f070..000000000 --- a/API/I18N/fi.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "generic-user-update": "Käyttäjää päivitettäessä oli poikkeus", - "manual-setup-fail": "Manuaalinen asennus ei ole mahdollista. Peruuta ja luo kutsu uudelleen", - "user-already-registered": "Käyttäjä on jo rekisteröity tunnuksella {0}", - "user-already-invited": "Käyttäjä on jo kutsuttu tällä sähköpostilla ja ei ole vielä hyväksynyt kutsua.", - "forgot-password-generic": "Sähköpostia lähetetään sähköpostiosoitteeseen, jos se on olemassa tietokannassamme", - "email-sent": "Sähköposti lähetetty", - "device-doesnt-exist": "Laitetta ei ole olemassa", - "generic-device-create": "Laitteen luomisessa tapahtui virhe", - "generic-device-update": "Laitteen päivittämisessä tapahtui virhe", - "send-to-kavita-email": "Laitteelle lähettämistä ei voi käyttää ilman sähköpostin asetuksia", - "send-to-unallowed": "Et voi lähettää laitteeseen, joka ei ole sinun", - "send-to-size-limit": "Tiedostot, joita yrität lähettää, ovat liian suuria sähköpostipalveluntarjoajallesi", - "send-to-device-status": "Tiedostoja siirretään laitteellesi", - "generic-send-to": "Tiedoston / tiedostojen lähettämisessä tapahtui virhe", - "no-cover-image": "Ei kansikuvaa", - "file-doesnt-exist": "Tiedostoa ei ole olemassa", - "user-doesnt-exist": "Käyttäjää ei ole olemassa", - "library-doesnt-exist": "Kirjastoa ei ole olemassa", - "invalid-path": "Virheellinen polku", - "bookmark-save": "Ei voitu tallentaa kirjanmerkkiä", - "cache-file-find": "Välimuistiin ladattua kuvaa ei löytynyt. Lataa ja yritä uudelleen.", - "name-required": "Nimi ei voi olla tyhjä", - "valid-number": "Täytyy olla voimassa oleva sivunumero", - "duplicate-bookmark": "Päällekkäinen kirjanmerkki on jo olemassa", - "reading-list-permission": "Sinulla ei ole lupaa tähän lukulistaan tai sitä ei ole olemassa", - "reading-list-position": "Sijaintia ei voitu päivittää", - "reading-list-updated": "Päivitetty", - "reading-list-deleted": "Lukulista poistettiin", - "generic-reading-list-create": "Lukulistan luomisessa tapahtui virhe", - "reading-list-doesnt-exist": "Lukulistaa ei ole olemassa", - "generic-scrobble-hold": "Säilytykseen siirtämisessä tapahtui virhe", - "no-series": "Ei saatu sarjoja Kirjastoon.", - "no-series-collection": "Ei saatu sarjaa kokoelmaan", - "generic-series-delete": "Sarjan poistamisessa kohdattiin ongelma", - "generic-series-update": "Sarjan päivittämisessä tapahtui virhe", - "series-updated": "Päivitetty onnistuneesti", - "update-metadata-fail": "Metatietoja ei voitu päivittää", - "age-restriction-not-applicable": "Ei rajoituksia", - "generic-relationship": "Suhteiden päivittämisessä oli ongelma", - "job-already-running": "Työ on jo käynnissä", - "encode-as-warning": "Et voi muuttaa PNG:ksi. Kansikuville, kokeile päivitystä. Kirjamerkkejä ja faviconeja ei voi koodata takaisin.", - "ip-address-invalid": "IP-osoite '{0}' on virheellinen", - "bookmark-dir-permissions": "Kirjanmerkki hakemistolla ei ole oikeita käyttöoikeuksia Kavitalle", - "total-backups": "Varmuuskopioiden kokonaismäärän on oltava välillä 1–30", - "total-logs": "Lokien kokonaismäärän on oltava välillä 1–30", - "stats-permission-denied": "Sinulla ei ole oikeutta katsoa toisen käyttäjän tilastoja", - "url-not-valid": "Url ei palauta kelvollista kuvaa tai vaatii luvan", - "url-required": "Sinun on annattava url käyttääksesi", - "reading-lists": "Lukulista", - "browse-libraries": "Selaa Kirjastoja", - "collections": "Kaikki kokoelmat", - "browse-collections": "Selaa Kokoelmia", - "smart-filters": "Älykkäät suodattimet", - "external-sources": "Ulkoiset lähteet", - "reading-list-restricted": "Lukulistaa ei ole olemassa tai sinulla ei ole pääsyä", - "search": "Haku", - "smart-filter-doesnt-exist": "Älykkäitä Suodattimia ei ole olemassa", - "unable-to-reset-k+": "Kavita+-lisenssiä ei voida palauttaa virheen vuoksi.Ota yhteyttä Kavita+ tukeen", - "theme-doesnt-exist": "Teema tiedosto puuttuu tai on virheellinen", - "epub-malformed": "Tiedosto on epämuodostunut! Ei voi lukea.", - "epub-html-missing": "Ei löytynyt sopivaa html:ää tälle sivulle", - "reading-list-title-required": "Luettelon otsikko ei voi olla tyhjä", - "collection-tag-title-required": "Kokoelman otsikko ei voi olla tyhjä", - "collection-tag-duplicate": "Kokoelma tällä nimellä on jo olemassa", - "device-duplicate": "On jo olemassa laite, jolla on tämä nimi", - "series-restricted-age-restriction": "Käyttäjä ei saa katsoa tätä sarjaa ikärajoitusten vuoksi", - "volume-num": "Nidos {0}", - "book-num": "Kirja {0}", - "issue-num": "Painos {0}{1}", - "check-updates": "Tarkista päivitykset", - "license-check": "Lisenssi tarkistus", - "report-stats": "Raporttitilat", - "scan-libraries": "Skannaa Kirjastot", - "confirm-email": "Sinun on vahvistettava sähköpostisi ensin", - "locked-out": "Sinut on lukittu ulos liian monen valtuutus yrityksen vuoksi. Ole hyvä ja odota 10 minuuttia.", - "disabled-account": "Sinun tilisi on poistettu käytöstä. Ota yhteyttä palvelimen ylläpitäjään.", - "register-user": "Jokin meni pieleen, kun käyttäjä rekisteröityi", - "validate-email": "Sähköpostin vahvistamisessa oli ongelma: {0}", - "confirm-token-gen": "Vahvistuspoletin luomisessa oli ongelma", - "denied": "Ei sallittu", - "permission-denied": "Tämä toiminto on sinulta kielletty", - "invalid-password": "Virheellinen Salasana", - "invalid-token": "Virheellinen merkki", - "invalid-payload": "Virheellinen hyötykuorma", - "nothing-to-do": "Ei mitään tekemistä", - "share-multiple-emails": "Et voi jakaa sähköpostia useilla tileillä", - "password-required": "Sinun on syötettävä olemassa oleva salasanasi muuttaaksesi tiliäsi, ellet ole järjestelmänvalvoja", - "unable-to-reset-key": "Jotain meni pieleen, ei pystytä nollaamaan avainta", - "generate-token": "Ongelmana oli vahvistus merkin luominen sähköpostitse. Katso virhelokit", - "age-restriction-update": "Ikärajoituksen päivittämisessä tapahtui virhe", - "no-user": "Käyttäjää ei ole olemassa", - "username-taken": "Käyttäjänimi on jo olemassa", - "user-already-confirmed": "Käyttäjä on jo vahvistettu", - "generic-invite-user": "Kutsuessa käyttäjää kohdattiin ongelma. Ole hyvä ja tarkista virhelokit.", - "invalid-email-confirmation": "Virheellinen sähköposti vahvistus", - "generic-user-email-update": "Et voi päivittää sähköpostia käyttäjälle. Tarkista lokit.", - "generic-password-update": "Odottamaton virhe uuden salasanan vahvistuksessa", - "password-updated": "Salasana päivitetty", - "not-accessible-password": "Palvelin ei ole käytettävissä. Linkki salasanan palauttamiseen on lokeissa", - "invalid-email": "Sähköposti käyttäjälle ei ole voimassa oleva sähköposti. Katso lokista mahdollisia linkkejä.", - "not-accessible": "Palvelin ei ole käytettävissä ulkoisesti", - "invalid-username": "Virheellinen käyttäjätunnus", - "user-migration-needed": "Tämä käyttäjä on pakko siirtää. Siirto tapahtuu ulos- ja sisäänkirjautumisen yhteydessä", - "admin-already-exists": "Admin on jo olemassa", - "generic-invite-email": "Sähköpostiviestin uudelleen lähettämisessä kohdattiin ongelma", - "critical-email-migration": "Sähköpostien siirron aikana tapahtui ongelma. Ota yhteyttä tukeen", - "email-not-enabled": "Sähköposti ei ole käytössä tällä palvelimella. Et voi suorittaa tätä toimintaa.", - "account-email-invalid": "Admin-tilin tiedostossa oleva sähköposti ei ole voimassa oleva sähköposti. Testi viestiä ei voi lähettää.", - "collection-deleted": "Kokoelma poistettu", - "generic-error": "Jokin meni pieleen, yritä uudelleen", - "email-settings-invalid": "Sähköposti asetuksista puuttuu tietoja. Varmista, että kaikki sähköposti asetukset on tallennettu.", - "chapter-doesnt-exist": "Lukua ei ole olemassa", - "collection-doesnt-exist": "Kokoelmaa ei ole olemassa", - "file-missing": "Tiedostoa ei löytynyt kirjasta", - "collection-updated": "Kokoelma päivitetty onnistuneesti", - "collection-already-exists": "Kokoelma on jo olemassa", - "error-import-stack": "MAL pinon tuonnissa tapahtui virhe", - "generic-device-delete": "Laitteen poistamisessa tapahtui virhe", - "greater-0": "{0} on oltava suurempi kuin 0", - "series-doesnt-exist": "Sarjaa ei ole olemassa", - "volume-doesnt-exist": "Nidosta ei ole olemassa", - "bookmarks-empty": "Kirjanmerkit eivät voi olla tyhjiä", - "bookmark-doesnt-exist": "Kirjanmerkkiä ei ole olemassa", - "must-be-defined": "{0} on määriteltävä", - "generic-favicon": "Verkkotunnuksen faviconin noutamisessa ilmeni ongelma", - "invalid-filename": "Virheellinen tiedostonimi", - "library-name-exists": "Kirjaston nimi on jo olemassa. Valitse palvelimelle yksilöllinen nimi.", - "generic-library": "Tapahtui kriittinen virhe. Kokeile uudestaan.", - "no-library-access": "Käyttäjällä ei ole pääsyä tähän kirjastoon", - "delete-library-while-scan": "Kirjastoa ei voi poistaa, kun skannaus on käynnissä. Odota, että skannaus suoritetaan tai uudelleenkäynnistä Kavita, ja yritä poistaa", - "generic-library-update": "Kirjastoa päivittäessä tapahtui kriittinen virhe.", - "invalid-access": "Virheellinen käyttöoikeus", - "pdf-doesnt-exist": "PDF:tä ei ole olemassa, vaikka pitäisi", - "no-image-for-page": "Ei tällaista kuvaa sivulla {0}. Yritä päivittää, jotta välimuisti voidaan tallentaa uudelleen.", - "perform-scan": "Tee skannaus sarjaan tai kirjastoon ja yritä uudelleen", - "generic-read-progress": "Edistyksen tallennuksessa tapahtui ongelma", - "bookmark-permission": "Sinulla ei ole lupaa lisätä / poistaa kirjanmerkkejä", - "generic-clear-bookmarks": "Kirjanmerkkejä ei voitu tyhjentää", - "reading-list-item-delete": "Kohdetta / kohteita ei voitu poistaa", - "generic-reading-list-delete": "Lukulistan poistamisessa tapahtui virhe", - "generic-reading-list-update": "Lukulistan päivityksessä tapahtui virhe", - "series-restricted": "Käyttäjällä ei ole pääsyä tähän sarjaan", - "libraries-restricted": "Käyttäjällä ei ole pääsyä kirjastoihin", - "generic-cover-series-save": "Ei voitu tallentaa kansikuvaa Sarjaan", - "generic-cover-collection-save": "Ei voitu tallentaa kansikuvaa Kokoelmaan", - "generic-cover-reading-list-save": "Ei voitu tallentaa kansikuvaa Lukulistaan", - "generic-cover-chapter-save": "Ei voitu tallentaa kansikuvaa Kappaleeseen", - "generic-cover-library-save": "Ei voitu tallentaa kansikuvaa Kirjastoon", - "generic-user-pref": "Asetusten tallentamisessa tapahtui virhe", - "generic-cover-person-save": "Ei voitu tallentaa kansikuvaa henkilölle", - "generic-cover-volume-save": "Ei voitu tallentaa kansikuvaa Nidokseen", - "access-denied": "Sinulla ei ole pääsyä", - "generic-user-delete": "Käyttäjää ei voitu poistaa", - "opds-disabled": "OPDS ei ole käytössä tällä palvelimella", - "recently-added": "Äskettäin lisätty", - "libraries": "Kaikki Kirjastot", - "browse-external-sources": "Selaa ulkoisia lähteitä", - "recently-updated": "Äskettäin päivitetty", - "browse-recently-updated": "Selaa äskettäin päivitettyjä", - "favicon-doesnt-exist": "Faviconia ei ole olemassa", - "search-description": "Haku Sarjoille, Kokoelmille tai Lukulistoille", - "external-source-already-exists": "Ulkoinen lähde on jo olemassa", - "external-source-required": "Apikey ja isäntä tarvittiin", - "external-source-doesnt-exist": "Ulkoista lähdettä ei ole olemassa", - "not-authenticated": "Käyttäjää ei ole todennettu", - "unable-to-register-k+": "Et voi rekisteröidä lisenssiä virheen vuoksi. Ota yhteyttä Kavita+ tukeen", - "device-not-created": "Tätä laitetta ei ole vielä olemassa. Luo ensin", - "reading-list-name-exists": "Tämän niminen Lukulista on jo olemassa", - "user-no-access-library-from-series": "Käyttäjällä ei ole pääsyä kirjastoon, johon sarja kuuluu", - "chapter-num": "Luku {0}", - "cleanup": "Puhdistus", - "browse-reading-lists": "Selaa Lukulistoja", - "person-doesnt-exist": "Henkilöä ei ole olemassa", - "person-name-required": "Henkilön nimi on pakollinen, eikä se saa olla tyhjä", - "person-name-unique": "Henkilön nimen on oltava yksilöllinen", - "person-image-doesnt-exist": "Henkilöä ei ole olemassa CoversDB:ssa", - "email-taken": "Sähköposti jo käytössä" -} diff --git a/API/I18N/fr.json b/API/I18N/fr.json index 2b9a4f81b..16be88960 100644 --- a/API/I18N/fr.json +++ b/API/I18N/fr.json @@ -1,26 +1,27 @@ { - "register-user": "Quelque chose s'est mal passé lors de l'enregistrement de l'utilisateur", + "register-user": "Une erreur est survenue lors de l'enregistrement de l'usager", "denied": "Interdit", - "permission-denied": "Vous n'êtes pas autorisé à cette opération", + "permission-denied": "Vous n'avez pas les permissions requises pour effectuer cette opération", "disabled-account": "Votre compte a été désactivé. Veuillez contacter un administrateur.", - "confirm-email": "Vous devez d'abord confirmer votre email", + "confirm-email": "Vous devez d'abord confirmer votre adresse courriel", "locked-out": "Vous avez été bloqués suite à un nombre trop élevé de tentatives. Veuillez réessayer dans 10 minutes.", - "validate-email": "Une erreur est survenue lors de la validation de votre courriel : {0}", - "confirm-token-gen": "Une erreur est survenue lors de la génération du jeton de confirmation", - "password-required": "Vous devez entrer votre mot de passe actuel pour changer votre compte à moins que vous ne soyez administrateur", + "bad-credentials": "Vos codes d'accès sont invalides", + "validate-email": "Une erreur est survenue lors de la validation de votre courriel : {0}", + "confirm-token-gen": "Une erreur est survenue lors de la génération du code de confirmation", + "password-required": "Vous devez entrer votre mot de passe existant afin de le changer si vous n'êtes pas un administrateur", "invalid-password": "Mot de passe invalide", - "invalid-token": "Jeton invalide", - "unable-to-reset-key": "Une erreur est survenue, impossible de réinitialiser la clé", - "generate-token": "Une erreur est survenue lors de la génération du jeton de confirmation de l'email. Voir les logs", + "invalid-token": "Code invalide", + "unable-to-reset-key": "Une erreur est survenue, impossible de générer la clé", + "generate-token": "Une erreur est survenue lors de la génération du code de confirmation du courriel. Voir le journal", "nothing-to-do": "Rien à faire", - "share-multiple-emails": "Vous ne pouvez pas partager un email sur plusieurs comptes", - "age-restriction-update": "Une erreur est survenue lors de la mise à jour de la restriction d'âge", - "no-user": "L'utilisateur n'existe pas", - "username-taken": "Le pseudo est déjà pris", - "user-already-confirmed": "L'utilisateur a déjà été confirmé", - "generic-user-update": "Une erreur est survenue lors de la mise à jour de l'utilisateur", - "user-already-registered": "L'utilisateur a déjà été enregistré en tant que {0}", - "user-already-invited": "L'utilisateur a déjà été invité avec cet email et n'a pas encore accepté l'invitation.", + "share-multiple-emails": "Vous ne pouvez partager une adresse courriel avec un autre compte", + "age-restriction-update": "Une erreur est survenue lors de la mise-à-jour de la restriction d'âge", + "no-user": "L'usager n'existe pas", + "username-taken": "Le nom d'usager existe déjà", + "user-already-confirmed": "L'usager à déjà été confirmé", + "generic-user-update": "Une erreur est survenue lors de la confirmation de l'usager", + "user-already-registered": "L'usager à déjà été enregistré en tant que {0}", + "user-already-invited": "L'usager à déjà été invité avec ce courriel et n'a pas encore accepté l'invitation.", "generic-invite-user": "Une erreur est survenue lors de l'invitation de l'usager. Voir le journal.", "invalid-email-confirmation": "La confirmation de courriel est invalide", "invalid-payload": "Payload invalide", @@ -40,7 +41,7 @@ "chapter-doesnt-exist": "Chapitre non existant", "file-missing": "Fichier introuvable dans le livre", "generic-device-delete": "Erreur lors de la suppression de l'appareil", - "send-to-kavita-email": "La fonction \"Envoyer à l'appareil\" ne peut pas être utilisée sans configurer l'email", + "send-to-kavita-email": "Envoyer à l'appareil ne peut pas être utilisé par le service e-mail de Kavita. Veuillez configurer le votre.", "generic-favicon": "Erreur lors de la récupération de la favicon pour le domaine", "generic-library": "Erreur critique. Essayez à nouveau.", "delete-library-while-scan": "Vous ne pouvez pas supprimer une bibliothèque lorsqu'une analyse est en cours. Veuillez attendre la fin de l'analyse ou redémarrez Kavita, puis essayez de la supprimer", @@ -121,7 +122,7 @@ "generic-user-delete": "Impossible de supprimer l'utilisateur", "generic-user-pref": "Erreur lors de la sauvegarde des préférences", "opds-disabled": "OPDS n'est pas activé sur ce serveur", - "on-deck": "En Cours", + "on-deck": "Continuez votre lecture", "recently-added": "Récemment Ajouté", "browse-recently-added": "Parcourir Récemment Ajouté", "reading-lists": "Liste de Lecture", @@ -159,55 +160,7 @@ "want-to-read": "À Lire", "browse-want-to-read": "Parcourir À Lire", "collection-deleted": "Collection supprimée", - "smart-filters": "Filtres intelligents", - "browse-smart-filters": "Recherche par filtres intelligents", - "smart-filter-doesnt-exist": "Aucun Filtres iintelligents n'existe", - "browse-external-sources": "Parcourir les Sources externes", - "external-sources": "Sources externes", - "external-source-already-in-use": "Il existe un flux avec cette source externe", - "dashboard-stream-doesnt-exist": "Le flux du tableau de bord n'existe pas", - "external-source-already-exists": "La source externe existe déjà", - "external-source-doesnt-exist": "La source externe n'existe pas", - "external-source-required": "La clé API et l'hôte sont requis", - "invalid-email": "L'email du fichier de l'utilisateur n'est pas un email valide. Voir les logs pour les liens.", - "sidenav-stream-doesnt-exist": "Le flux de la barre de navigation latérale n'existe pas", - "smart-filter-already-in-use": "Il existe un flux avec ce filtre intelligent", - "browse-more-in-genre": "Parcourir plus dans {0}", - "more-in-genre": "Plus dans le genre {0}", - "recently-updated": "Récemment mis à jour", - "browse-recently-updated": "Parcourir les mises à jour récentes", - "unable-to-reset-k+": "Impossible de réinitialiser la licence Kavita+ en raison d'une erreur. Contactez le support Kavita+", - "email-not-enabled": "E-mail non activé sur ce serveur. Vous ne pouvez pas lancer cette action.", - "send-to-unallowed": "Vous ne pouvez envoyer à un appareil qui ne vous appartient pas", - "send-to-size-limit": "Le(s) fichier(s) que vous essayez d'envoyer est (sont) trop volumineux pour votre fournisseur d'email", - "check-updates": "Vérifier les mises à jour", - "license-check": "Vérification de la licence", - "cleanup": "Nettoyage", - "report-stats": "Rapport des statistiques", - "process-scrobbling-events": "Traiter les événements de Scrobbling", - "check-scrobbling-tokens": "Vérifier les jetons de Scrobbling", - "remove-from-want-to-read": "Nettoyage de la liste d'envie (déjà lu)", - "scan-libraries": "Analyse des bibliothèques", - "kavita+-data-refresh": "Actualisation des données Kavita+", - "backup": "Sauvegarde", - "process-processed-scrobbling-events": "Traiter les événements de Scrobbling traités", - "update-yearly-stats": "Mettre à jour les statistiques annuelles", - "account-email-invalid": "L'adresse électronique figurant dans le fichier du compte administrateur n'est pas valide. Impossible d'envoyer un courriel de test.", - "email-settings-invalid": "Informations manquantes dans les paramètres de l'email. Assurez-vous que tous les paramètres de l'email sont sauvegardés.", - "collection-already-exists": "Collection déjà existante", - "error-import-stack": "Il y a eu un problème lors de l'importation de la pile MAL", - "generic-cover-person-save": "Impossible d'enregistrer l'image de couverture pour cette personne", - "generic-cover-volume-save": "Impossible d'enregistrer l'image de couverture sur le volume", - "person-name-unique": "Le nom de la personne doit être unique", - "person-image-doesnt-exist": "La personne n'existe pas dans CoversDB", - "person-doesnt-exist": "La personne n'existe pas", - "person-name-required": "Le nom de la personne est obligatoire et ne doit pas être nul", - "email-taken": "Email déjà existant", - "kavitaplus-restricted": "Ce service est réservé à Kavita+", - "sidenav-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés de la SideNav", - "dashboard-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés du tableau de bord", - "smart-filter-name-required": "Nom du filtre intelligent requis", - "smart-filter-system-name": "Vous ne pouvez pas utiliser le nom d'un flux fourni par le système", - "aliases-have-overlap": "Un ou plusieurs alias se chevauchent avec d'autres personnes et ne peuvent pas être mis à jour", - "generated-reading-profile-name": "Généré à partir de {0}" + "smart-filters": "Filtres iintelligents", + "browse-smart-filters": "Parcourir Filtres intelligents", + "smart-filter-doesnt-exist": "Aucun Filtres iintelligents n'existe" } diff --git a/API/I18N/ga.json b/API/I18N/ga.json deleted file mode 100644 index 142425aec..000000000 --- a/API/I18N/ga.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "confirm-email": "Ní mór duit do ríomhphost a dhearbhú ar dtús", - "disabled-account": "Tá do chuntas díchumasaithe. Déan teagmháil le riarthóir an fhreastalaí.", - "validate-email": "Bhí fadhb ann agus do ríomhphost á bhailíochtú: {0}", - "confirm-token-gen": "Bhí fadhb ann agus comhartha deimhnithe á ghiniúint", - "denied": "Ní cheadaítear", - "permission-denied": "Níl cead agat an oibríocht seo a dhéanamh", - "invalid-password": "Pasfhocal Neamhbhailí", - "invalid-token": "Comhartha neamhbhailí", - "unable-to-reset-key": "Tharla earráid, níorbh fhéidir an eochair a athshocrú", - "invalid-payload": "Ualach neamhbhailí", - "nothing-to-do": "Tada le déanamh", - "share-multiple-emails": "Ní féidir leat ríomhphoist a roinnt thar níos mó ná cuntas amháin", - "generate-token": "Bhí fadhb ann agus comhartha ríomhphoist deimhnithe á ghiniúint. Féach logs", - "age-restriction-update": "Tharla earráid agus an srian aoise á nuashonrú", - "no-user": "Níl an t-úsáideoir ann", - "username-taken": "Ainm úsáideora tógtha cheana féin", - "user-already-confirmed": "Úsáideoir deimhnithe cheana féin", - "generic-user-update": "Bhí eisceacht ann nuair a bhí an t-úsáideoir á nuashonrú", - "manual-setup-fail": "Ní féidir an socrú láimhe a chur i gcrích. Cuir ar ceal agus athchruthaigh an cuireadh", - "user-already-invited": "Tá cuireadh faighte ag an úsáideoir faoin ríomhphost seo cheana féin agus níor ghlac sé leis an gcuireadh fós.", - "invalid-email-confirmation": "Deimhniú ríomhphoist neamhbhailí", - "generic-user-email-update": "Ní féidir ríomhphost a nuashonrú don úsáideoir. Seiceáil logs.", - "generic-password-update": "Tharla earráid gan choinne agus pasfhocal nua á dheimhniú", - "password-updated": "Pasfhocal Nuashonraithe", - "forgot-password-generic": "Seolfar ríomhphost chuig an ríomhphost má tá sé inár mbunachar sonraí", - "not-accessible-password": "Níl do fhreastalaí inrochtana. Tá an nasc chun do phasfhocal a athshocrú sna logaí", - "invalid-email": "Ní ríomhphost bailí é an ríomhphost atá ar comhad don úsáideoir. Féach logaí le haghaidh naisc ar bith.", - "email-sent": "Ríomhphost seolta", - "user-migration-needed": "Ní mór don úsáideoir seo dul ar imirce. Iarr orthu logáil amach agus logáil isteach chun sreabhadh imirce a spreagadh", - "generic-invite-email": "Bhí fadhb ann agus an ríomhphost cuireadh á athsheoladh", - "admin-already-exists": "Tá riarachán ann cheana féin", - "invalid-username": "Ainm Úsáideora neamhbhailí", - "critical-email-migration": "Bhí fadhb ann le linn aistriú ríomhphoist. Tacaíocht teagmhála", - "email-not-enabled": "Níl ríomhphost cumasaithe ar an bhfreastalaí seo. Ní féidir leat an gníomh seo a dhéanamh.", - "email-settings-invalid": "Eolas in easnamh i socruithe ríomhphoist. Cinntigh go ndéantar gach socrú ríomhphoist a shábháil.", - "file-missing": "Ní bhfuarthas comhad sa leabhar", - "collection-updated": "D'éirigh leis an mbailiúchán a nuashonrú", - "generic-error": "Tharla earráid, bain triail eile as", - "error-import-stack": "Bhí fadhb ann maidir le stoic MAL a iompórtáil", - "device-doesnt-exist": "Níl an gléas ann", - "generic-device-create": "Tharla earráid agus an gléas á chruthú", - "generic-device-update": "Tharla earráid agus an gléas á nuashonrú", - "send-to-kavita-email": "Ní féidir seoladh chuig an ngléas a úsáid gan R-phost a shocrú", - "send-to-unallowed": "Ní féidir leat seoladh chuig gléas nach leatsa é", - "send-to-size-limit": "Tá an comhad/na comhaid atá tú ag iarraidh a sheoladh rómhór do do sholáthraí ríomhphoist", - "send-to-device-status": "Comhaid a aistriú chuig do ghléas", - "must-be-defined": "Ní mór {0} a shainiú", - "generic-favicon": "Bhí fadhb ann maidir le favicon a fháil don fhearann", - "invalid-filename": "Ainm Comhaid Neamhbhailí", - "file-doesnt-exist": "Níl an comhad ann", - "generic-library": "Bhí ceist chriticiúil ann. Arís, le d'thoil.", - "user-doesnt-exist": "Níl an t-úsáideoir ann", - "library-doesnt-exist": "Níl an leabharlann ann", - "invalid-path": "Conair Neamhbhailí", - "delete-library-while-scan": "Ní féidir leat leabharlann a scriosadh agus scanadh ar siúl. Fan go mbeidh an scanadh críochnaithe nó chun Kavita a atosú agus ansin déan iarracht é a scriosadh", - "generic-library-update": "Bhí ceist ríthábhachtach ag baint le nuashonrú na leabharlainne.", - "pdf-doesnt-exist": "Níl PDF ann nuair ba chóir", - "invalid-access": "Rochtain Neamhbhailí", - "no-image-for-page": "Níl a leithéid d'íomhá don leathanach {0}. Bain triail as athnuachan chun ath-taisce a cheadú.", - "perform-scan": "Déan scanadh ar an tsraith nó ar an leabharlann seo agus bain triail eile as", - "generic-read-progress": "Bhí fadhb ann maidir le dul chun cinn a shábháil", - "generic-clear-bookmarks": "Níorbh fhéidir leabharmharcanna a ghlanadh", - "bookmark-permission": "Níl cead agat leabharmharcáil/dí-leabharmharc a dhéanamh", - "bookmark-save": "Níorbh fhéidir leabharmharc a shábháil", - "cache-file-find": "Níorbh fhéidir íomhá i dtaisce a aimsiú. Athlódáil agus bain triail eile as.", - "name-required": "Ní féidir leis an ainm a bheith folamh", - "duplicate-bookmark": "Tá iontráil leabharmharc dúblach ann cheana féin", - "reading-list-permission": "Níl ceadanna agat ar an liosta léitheoireachta seo nó níl an liosta ann", - "reading-list-position": "Níorbh fhéidir an t-ionad a nuashonrú", - "reading-list-updated": "Nuashonraithe", - "reading-list-item-delete": "Níorbh fhéidir an mhír(eanna) a scriosadh", - "reading-list-deleted": "Scriosadh an Liosta Léitheoireachta", - "generic-reading-list-delete": "Bhí fadhb ann agus an liosta léitheoireachta á scriosadh", - "generic-reading-list-create": "Bhí fadhb ann agus an liosta léitheoireachta á chruthú", - "generic-scrobble-hold": "Tharla earráid agus an coimeád á chur leis", - "libraries-restricted": "Níl rochtain ag an úsáideoir ar aon leabharlanna", - "no-series": "Níorbh fhéidir sraith a fháil don Leabharlann", - "no-series-collection": "Níorbh fhéidir sraith a fháil le haghaidh Bailiúchán", - "generic-series-delete": "Bhí fadhb ann an tsraith a scriosadh", - "generic-series-update": "Tharla earráid agus an tsraith á nuashonrú", - "series-updated": "D'éirigh le nuashonrú", - "age-restriction-not-applicable": "Gan srian", - "generic-relationship": "Bhí fadhb ann maidir le caidrimh a nuashonrú", - "job-already-running": "Job ar siúl cheana féin", - "ip-address-invalid": "Seoladh IP '{0}' neamhbhailí", - "bookmark-dir-permissions": "Níl na ceadanna cearta ag an Eolaire Leabharmharcanna chun Kavita a úsáid", - "total-backups": "Caithfidh Cúltaca Iomlána a bheith idir 1 agus 30", - "total-logs": "Caithfidh an Logchomhaid Iomlán a bheith idir 1 agus 30", - "url-not-valid": "Ní sheolann URL íomhá bhailí ar ais nó éilíonn sé údarú", - "url-required": "Caithfidh tú pas a fháil i url le húsáid", - "generic-cover-series-save": "Ní féidir íomhá an chlúdaigh a shábháil sa tSraith", - "generic-cover-collection-save": "Ní féidir íomhá an chlúdaigh a shábháil sa Bhailiúchán", - "generic-cover-reading-list-save": "Ní féidir íomhá an chlúdaigh a shábháil ar an Liosta Léitheoireachta", - "generic-cover-volume-save": "Ní féidir íomhá an chlúdaigh a shábháil in Volume", - "access-denied": "Níl rochtain agat", - "reset-chapter-lock": "Ní féidir glas clúdaigh a athshocrú do Chapter", - "generic-user-delete": "Níorbh fhéidir an t-úsáideoir a scriosadh", - "opds-disabled": "Níl OPDS cumasaithe ar an bhfreastalaí seo", - "on-deck": "Ar Deic", - "browse-on-deck": "Brabhsáil Ar Deic", - "recently-added": "Leis Le Déanaí", - "want-to-read": "Ba mhaith liom a léamh", - "browse-reading-lists": "Brabhsáil de réir Liostaí Léitheoireachta", - "libraries": "Gach Leabharlann", - "browse-libraries": "Brabhsáil de réir Leabharlanna", - "collections": "Gach Bailiúchán", - "browse-recently-updated": "Brabhsáil Nuashonraithe Le Déanaí", - "smart-filters": "Scagairí Cliste", - "external-sources": "Foinsí Seachtracha", - "browse-external-sources": "Brabhsáil Foinsí Seachtracha", - "browse-smart-filters": "Brabhsáil de réir Scagairí Cliste", - "reading-list-restricted": "Níl an liosta léitheoireachta ann nó níl rochtain agat", - "query-required": "Caithfidh tú pas a fháil i bparaiméadar fiosrúcháin", - "search": "Cuardach", - "search-description": "Cuardaigh Sraitheanna, Bailiúcháin, nó Liostaí Léitheoireachta", - "favicon-doesnt-exist": "Níl Favicon ann", - "smart-filter-doesnt-exist": "Níl Smart Scagaire ann", - "smart-filter-already-in-use": "Tá sruth leis an Scagaire Cliste seo cheana féin", - "dashboard-stream-doesnt-exist": "Níl Sruth an Deais ann", - "sidenav-stream-doesnt-exist": "Níl Sruth SideNav ann", - "external-source-already-exists": "Tá Foinse Seachtrach ann cheana féin", - "external-source-required": "ApiKey agus Óstach ag teastáil", - "external-source-doesnt-exist": "Níl Foinse Sheachtrach ann", - "external-source-already-in-use": "Tá sruth leis an bhFoinse Sheachtrach seo cheana féin", - "not-authenticated": "Níl an t-úsáideoir fíordheimhnithe", - "unable-to-register-k+": "Ní féidir ceadúnas a chlárú mar gheall ar earráid. Déan teagmháil le Tacaíocht Kavita+", - "anilist-cred-expired": "Tá Dintiúir AniList imithe in éag nó gan a bheith socraithe", - "scrobble-bad-payload": "Droch-ualú pá ó Sholáthraí Scrobble", - "theme-doesnt-exist": "Comhad téama in easnamh nó neamhbhailí", - "generic-create-temp-archive": "Bhí fadhb ann agus cartlann ama á cruthú", - "epub-html-missing": "Níorbh fhéidir an html cuí a aimsiú don leathanach sin", - "collection-tag-title-required": "Ní féidir le Teideal an Bhailiúcháin a bheith folamh", - "collection-tag-duplicate": "Tá bailiúchán leis an ainm seo ann cheana féin", - "device-duplicate": "Tá gléas leis an ainm seo ann cheana", - "send-to-permission": "Ní féidir neamh-EPUB nó PDF a sheoladh chuig gléasanna mar nach dtacaítear leo ar Kindle", - "progress-must-exist": "Caithfidh dul chun cinn a bheith ann ar an úsáideoir", - "reading-list-name-exists": "Tá liosta léitheoireachta den ainm seo ann cheana", - "user-no-access-library-from-series": "Níl rochtain ag an úsáideoir ar an leabharlann lena mbaineann an tsraith seo", - "volume-num": "Imleabhar {0}", - "book-num": "Leabhar {0}", - "issue-num": "Eagrán {0}{1}", - "chapter-num": "Caibidil {0}", - "check-updates": "Seiceáil Nuashonruithe", - "license-check": "Seiceáil Ceadúnais", - "process-scrobbling-events": "Próiseáil Imeachtaí Scroblach", - "report-stats": "Staitisticí Tuairisce", - "check-scrobbling-tokens": "Seiceáil Comharthaí Scrobbling", - "cleanup": "Glan Suas", - "process-processed-scrobbling-events": "Imeachtaí Scrobarnach Próiseáilte a Phróiseáil", - "remove-from-want-to-read": "Glanadh Ba Mhaith Liom a Léamh", - "scan-libraries": "Scanadh Leabharlanna", - "kavita+-data-refresh": "Kavita+ Athnuachan Sonraí", - "backup": "Cúltaca", - "update-yearly-stats": "Nuashonraigh na Staitisticí Bliantúla", - "account-email-invalid": "Ní ríomhphost bailí é an ríomhphost atá ar comhad don chuntas riaracháin. Ní féidir ríomhphost tástála a sheoladh.", - "locked-out": "Glasáladh amach thú as an iomarca iarrachtaí údaraithe. Fan 10 nóiméad le do thoil.", - "register-user": "Tharla earráid agus úsáideoir á chlárú", - "password-required": "Ní mór duit do phasfhocal reatha a chur isteach chun do chuntas a athrú ach amháin más riarthóir thú", - "generic-invite-user": "Bhí fadhb ann le cuireadh a thabhairt don úsáideoir. Seiceáil na logaí le do thoil.", - "user-already-registered": "Tá an t-úsáideoir cláraithe mar {0} cheana", - "not-accessible": "Níl rochtain sheachtrach ar do fhreastalaí", - "collection-deleted": "Bailiúchán scriosta", - "series-doesnt-exist": "Níl an tsraith ann", - "bookmark-doesnt-exist": "Níl leabharmharc ann", - "collection-doesnt-exist": "Níl bailiúchán ann", - "generic-send-to": "Tharla earráid agus an comhad/na comhaid á seoladh chuig an ngléas", - "volume-doesnt-exist": "Níl toirt ann", - "bookmarks-empty": "Ní féidir le leabharmharcanna a bheith folamh", - "chapter-doesnt-exist": "Níl Caibidil ann", - "no-cover-image": "Gan íomhá clúdaigh", - "generic-cover-library-save": "Ní féidir íomhá an chlúdaigh a shábháil sa Leabharlann", - "collection-already-exists": "Tá bailiúchán ann cheana féin", - "generic-device-delete": "Tharla earráid agus an gléas á scriosadh", - "greater-0": "Caithfidh {0} a bheith níos mó ná 0", - "library-name-exists": "Tá ainm na leabharlainne ann cheana féin. Roghnaigh ainm ar leith don fhreastalaí.", - "no-library-access": "Níl rochtain ag an úsáideoir ar an leabharlann seo", - "valid-number": "Caithfidh gur uimhir leathanaigh bhailí é", - "generic-reading-list-update": "Bhí fadhb ann an liosta léitheoireachta a nuashonrú", - "reading-list-doesnt-exist": "Níl liosta léitheoireachta ann", - "series-restricted": "Níl rochtain ag an úsáideoir ar an Sraith seo", - "update-metadata-fail": "Níorbh fhéidir meiteashonraí a nuashonrú", - "encode-as-warning": "Ní féidir leat tiontú go PNG. Le haghaidh clúdaigh, bain úsáid as Clúdaigh Athnuaigh. Ní féidir leabharmharcanna agus favicons a ionchódú ar ais.", - "stats-permission-denied": "Níl tú údaraithe chun féachaint ar staitisticí úsáideora eile", - "generic-cover-chapter-save": "Ní féidir íomhá an chlúdaigh a shábháil ar Chapter", - "generic-cover-person-save": "Ní féidir íomhá an chlúdaigh a shábháil don Duine", - "generic-user-pref": "Bhí fadhb ann maidir le roghanna a shábháil", - "reading-lists": "Liostaí Léitheoireachta", - "browse-collections": "Brabhsáil de réir Bailiúcháin", - "browse-more-in-genre": "Brabhsáil tuilleadh in {0}", - "recently-updated": "Nuashonraithe Le Déanaí", - "reading-list-title-required": "Ní féidir le Teideal an Liosta Léitheoireachta a bheith folamh", - "device-not-created": "Níl an gléas seo ann fós. Cruthaigh ar dtús le do thoil", - "browse-recently-added": "Brabhsáil Curtha Leis Le Déanaí", - "browse-want-to-read": "Brabhsáil Ba Mhaith Liom a Léamh", - "more-in-genre": "Tuilleadh sa Seánra {0}", - "unable-to-reset-k+": "Ní féidir ceadúnas Kavita+ a athshocrú de bharr earráide. Déan teagmháil le Tacaíocht Kavita+", - "series-restricted-age-restriction": "Níl cead ag an úsáideoir an tsraith seo a fheiceáil mar gheall ar shrianta aoise", - "bad-copy-files-for-download": "Ní féidir na comhaid a chóipeáil go dtí an chartlann eolaire sealadach íoslódáil.", - "epub-malformed": "Tá an comhad míchumtha! Ní féidir a léamh.", - "person-doesnt-exist": "Níl duine ann", - "person-name-required": "Tá ainm an duine ag teastáil agus ní féidir é a bheith ar neamhní", - "person-name-unique": "Caithfidh ainm duine a bheith uathúil", - "person-image-doesnt-exist": "Níl an duine in CoversDB", - "email-taken": "Ríomhphost in úsáid cheana féin", - "kavitaplus-restricted": "Tá sé seo teoranta do Kavita+ amháin", - "smart-filter-system-name": "Ní féidir leat ainm srutha an chórais a sholáthair tú a úsáid", - "sidenav-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh as an SideNav", - "dashboard-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh ón deais", - "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil", - "aliases-have-overlap": "Tá forluí idir ceann amháin nó níos mó de na leasainmneacha agus daoine eile, ní féidir iad a nuashonrú", - "generated-reading-profile-name": "Gineadh ó {0}" -} diff --git a/API/I18N/he.json b/API/I18N/he.json index 3b2386bf6..fbb115271 100644 --- a/API/I18N/he.json +++ b/API/I18N/he.json @@ -1,6 +1,7 @@ { "confirm-email": "חובה לאמת תחילה כתובת דואר אלקטרוני", "denied": "לא מאושר", + "bad-credentials": "שם משתמש או סיסמא לא נכונים", "locked-out": "חשבונך ננעל לאחר מספר מקסימלי של נסיונות כניסה לא מוצלחים. אנא המתן/ני 10 דקות.", "disabled-account": "חשבונך לא פעיל. אנא פנה למנהל המערכת.", "validate-email": "אירעה תקלה בעת ניסיון וידוא כתובת הדואר האלקטרוני שלך: {0}", @@ -18,9 +19,5 @@ "no-user": "משתמש לא קיים", "username-taken": "שם משתמש תפוס", "user-already-confirmed": "המשתמש כבר אושר", - "age-restriction-update": "אירעה תקלה בעת עדכון הגבלת גיל", - "generic-user-update": "אירעה תקלה בעת עדכון משתמש", - "user-already-registered": "משתמש רשום כבר בתור {0}", - "manual-setup-fail": "לא מתאפשר להשלים הגדרה ידנית. יש לבטל וליצור מחדש את ההזמנה", - "email-taken": "דואר אלקטרוני כבר בשימוש" + "age-restriction-update": "אירעה תקלה בעת עדכון הגבלת גיל" } diff --git a/API/I18N/hi.json b/API/I18N/hi.json index 5e1ea21f6..5fb9edd0a 100644 --- a/API/I18N/hi.json +++ b/API/I18N/hi.json @@ -67,6 +67,7 @@ "volume-num": "वॉल्यूम {0}", "book-num": "बुक {0}", "issue-num": "अंक(Issue) {0}{1}", + "bad-credentials": "आपकी क्रेडेंशियल सही नहीं हैं", "locked-out": "आपको कई प्राधिकरण प्रयासों के कारण बंद कर दिया गया है। कृपया 10 मिनट प्रतीक्षा करें।।", "register-user": "उपयोगकर्ता पंजीकरण करते समय कुछ गलत हो गया", "disabled-account": "आपका खाता अक्षम है। सर्वर व्यवस्थापक से संपर्क करें।।", diff --git a/API/I18N/hu.json b/API/I18N/hu.json deleted file mode 100644 index 21649e2ce..000000000 --- a/API/I18N/hu.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "disabled-account": "A fiókod le lett tiltva. Vedd fel a kapcsolatot a szerver adminnal.", - "permission-denied": "Ez a művelet nem engedélyezett", - "password-required": "Be kell írnod a meglévő jelszavadatat a fiók megváltoztatásához, kivéve ha admin vagy", - "unable-to-reset-key": "Probléma a kulcs visszaállítása során", - "invalid-payload": "Érvénytelen adat", - "generate-token": "Probléma a megerősítő e-mail token generálása során. Lásd a logokat", - "age-restriction-update": "Probléma a korhatár frissítésénél", - "username-taken": "A felhasználónév foglalt", - "generic-user-update": "Hiba a felhasználó frissítésekor", - "user-already-invited": "A felhasználót már meghívták e-mailben, de még nem fogadta el.", - "generic-user-email-update": "Nem lehet frissíteni a felhasználó e-mail címét. Kérlek ellenőrizd a logokat.", - "generic-password-update": "Váratlan hiba az új jelszó megerősítésekor", - "forgot-password-generic": "Küldünk egy e-mailt, ha a cím szerepel az adatbázisunkban", - "not-accessible": "A szervered kívülről nem érhető el", - "email-sent": "E-mail elküldve", - "generic-invite-email": "Probléma volt a meghívó e-mail újraküldésekor", - "email-not-enabled": "Az e-mail nem engedélyezett ezen a szerveren. A feladat nem indítható el.", - "email-settings-invalid": "Az e-mail beállításokból információk hiányoznak. Győződj meg róla hogy minden e-mail beállítás el van mentve.", - "collection-deleted": "Gyűjtemény törölve", - "generic-error": "Valami nem jó, kérlek próbáld újra", - "collection-doesnt-exist": "A gyűjtemény nem létezik", - "device-doesnt-exist": "Az eszköz nem létezik", - "generic-device-update": "Probléma volt az eszköz frissítésével", - "send-to-unallowed": "Nem küldhetsz eszközre ami nem a tiéd", - "send-to-size-limit": "A file(ok) amiket küldeni próbálsz túl nagyok az e-mailhez", - "send-to-device-status": "Fileok átvitele az eszközödre", - "generic-send-to": "Probléma volt a file(ok) eszközre küldésével", - "volume-doesnt-exist": "A kötet nem létezik", - "library-name-exists": "A könyvtár név már létezik. Válassz egyedi nevet a szerveren.", - "generic-library": "Volt egy kritikus probléma. Kérlek próbáld újra.", - "no-library-access": "A felhasználónak nincs hozzáférése ehhez a könyvtárhoz", - "library-doesnt-exist": "A könyvtár nem létezik", - "generic-library-update": "Kritikus probléma a könyvtár frissítése közben.", - "no-image-for-page": "Nincs kép a(z) {0} oldalhoz. Frissíts az újra cache-eléshez.", - "generic-read-progress": "Probléma volt a mentés közben", - "generic-clear-bookmarks": "Nem lehetett a könyvjelzőket kiüríteni", - "bookmark-save": "Nem lehet a könyvjelzőt menteni", - "valid-number": "Az oldalszámnak érvényesnek kell lennie", - "duplicate-bookmark": "Már létező könyvjelző", - "reading-list-permission": "Nincs hozzáférésed a lista olvasásához vagy a lista nem létezik", - "reading-list-updated": "Frissítve", - "generic-reading-list-update": "Probléma volt az olvasási lista frissítése közben", - "series-restricted": "A felhasználónak nincs joga ehhez a sorozathoz", - "confirm-email": "Erősítsd meg az e-mail címedet először", - "register-user": "Felhasználó regisztrálása sikertelen", - "invalid-password": "Érvénytelen jelszó", - "invalid-token": "Érvénytelen token", - "locked-out": "Túl sok hibás bejelentkezési próbálkozás. Kérlek várj 10 percet.", - "validate-email": "Probléma volt az e-mail címed ellenőrzésekor: {0}", - "share-multiple-emails": "Az e-mail cím megosztása nem lehetséges több fiók között", - "confirm-token-gen": "Probléma a megerősítő token generálása során", - "denied": "Nem engedélyezett", - "no-user": "A felhasználó nem létezik", - "user-already-confirmed": "A felhasználó már meg van erősítve", - "manual-setup-fail": "A kézi beállítást nem lehet befejezni. Kérlek töröld és készítsd el újra a meghívót", - "user-already-registered": "A felhasználó már regisztrált, mint {0}", - "generic-invite-user": "Probléma volt a felhasználó meghívásánál. Kérlek ellenőrizd a logokat.", - "invalid-email-confirmation": "Érvénytelen e-mail megerősítés", - "password-updated": "Jelszó frissítve", - "not-accessible-password": "A szerver nem elérhető. A jelszó visszaállításához való hivatkozás a logokban található", - "nothing-to-do": "Nincs mit tenni", - "user-migration-needed": "A felhasználót migrálni kell. Kérd meg hogy lépjen ki és be, hogy a migráció elkezdődjön", - "critical-email-migration": "Probléma volt az e-mail migráció közben. Vedd fel a kapcsolatot a supporttal", - "admin-already-exists": "Az Admin már létezik", - "invalid-username": "Érvénytelen felhasználónév", - "account-email-invalid": "Az e-mail cím az admin fiókhoz nem érvényes. A teszt e-mail nem küldhető el.", - "invalid-email": "A felhasználóhoz rendelt e-mai cím nem érvényes. Lásd a logokat a hivatkozásokhoz.", - "chapter-doesnt-exist": "A fejezet nem létezik", - "file-missing": "A fájl nem található a könyvben", - "collection-updated": "A gyűjtemény sikeresen frissítve", - "collection-already-exists": "A gyűjtemény már létezik", - "error-import-stack": "Probléma volt a „MAL stack” importálásakor", - "generic-device-create": "Probléma volt az eszköz készítésével", - "generic-device-delete": "Probléma volt az eszköz törlésével", - "greater-0": "{0} nagyobbnak kell lennie mint 0", - "send-to-kavita-email": "Az „Eszközre küldés” nem használható e-mail beállítás nélkül", - "series-doesnt-exist": "A sorozat nem létezik", - "bookmarks-empty": "A könyvjelzők nem lehetnek üresek", - "no-cover-image": "Nincs borítókép", - "bookmark-doesnt-exist": "A könyvjelző nem létezik", - "generic-favicon": "Probléma volt a domainhez tartozó favicon lekérésekor", - "must-be-defined": "{0} definiálni kell", - "invalid-filename": "Hibás filenév", - "file-doesnt-exist": "A fájl nem létezik", - "user-doesnt-exist": "A felhasználó nem létezik", - "invalid-path": "Hibás útvonal", - "delete-library-while-scan": "Nem törölhetsz könyvtárat a scannelés közben. Kérlek várj míg a scannelés befejeződik vagy indítsd újra a Kavitát és próbáld újra a törlést", - "pdf-doesnt-exist": "Nem létezik a PDF, pedig kellene", - "invalid-access": "Hibás hozzáférés", - "perform-scan": "Kérlek scanneld újra ezt a sorozatot vagy könyvtárat és próbáld újra", - "bookmark-permission": "Nincs jogosultságod a könyvjelző készítéshez/törléshez", - "cache-file-find": "Nem lehetett a cache-elt képet megtalálni. Töltsd újra és próbáld még egyszer.", - "name-required": "A név nem lehet üres", - "reading-list-position": "Nem lehet a pozíciót frissíteni", - "reading-list-item-delete": "Nem lehet törölni a tétel(eket)", - "reading-list-deleted": "Olvasási lista törölve", - "generic-reading-list-delete": "Probléma volt az olvasási lista törlése közben", - "generic-reading-list-create": "Probléma volt az olvasási lista készítése közben", - "reading-list-doesnt-exist": "Az olvasási lista nem létezik", - "libraries-restricted": "A felhasználónak nincs könyvtár hozzáférése", - "generic-scrobble-hold": "Hiba történt a \"hold\" hozzáadásakor", - "no-series": "Nem lehet sorozatot találni a könyvtárhoz", - "no-series-collection": "Nem lehet sorozatot találni a gyűjteményhez", - "generic-series-delete": "Probléma volt a sorozat törlése közben", - "generic-series-update": "Probléma volt a sorozat frissítése közben", - "update-metadata-fail": "Nem lehet a metaadatot frissíteni", - "series-updated": "Sikeresen frissítve", - "age-restriction-not-applicable": "Nincs korlátozás", - "generic-relationship": "Probléma volt a kapcsolatok frissítése közben", - "job-already-running": "Már fut a munkamenet", - "encode-as-warning": "Nem konvertálhasz PNG-be. Borítókhoz használd a Borítók frissítését. Könyvjelzőket és Favikononkat nem lehet vissza kódolni.", - "ip-address-invalid": "IP cím '{0}' nem érvényes", - "bookmark-dir-permissions": "Könyvjelző könyvtárnak nincs helyes jogosultsága a Kavitának", - "total-backups": "Az Összes Mentésnek 1 és 30 között kell lennie", - "total-logs": "Az Összes Logoknak 1 és 30 között kell lennie", - "stats-permission-denied": "Nincs jogosultságod másik felhasználó statisztikáját megnézni", - "url-not-valid": "URL nem ad vissza valódi képet, vagy bejelentkezés szükséges", - "url-required": "Be kell írnod egy URL-t a használathoz", - "generic-cover-series-save": "Nem lehet borítóképet menteni a sorozathoz", - "generic-cover-collection-save": "Nem lehet borítóképet menteni a gyűjteményhez", - "generic-cover-reading-list-save": "Nem lehet borítóképet menteni az olvasási listához", - "generic-cover-library-save": "Nem lehet borítóképet menteni a könyvtárhoz", - "access-denied": "Nincs hozzáférésed", - "generic-cover-chapter-save": "Nem lehet borítóképet menteni a fejezethez", - "reset-chapter-lock": "Nem lehet a fejezet borító rögzítését visszaállítani", - "generic-user-delete": "Nem lehet törölni a felhasználót", - "generic-user-pref": "Probléma volt a beállítások mentésénél", - "opds-disabled": "Az ODPS nincs engedélyezve ezen a szerveren", - "on-deck": "A Fedélzeten", - "browse-on-deck": "Böngéssz a fedélzeten", - "recently-added": "Nemrég hozzáadott", - "want-to-read": "Olvasandó", - "browse-want-to-read": "Olvasandó böngészése", - "browse-recently-added": "Nemrég hozzáadott böngészése", - "reading-lists": "Olvasási Listák", - "libraries": "Összes Könyvtár", - "browse-libraries": "Könyvtárak böngészése", - "collections": "Összes gyűjtemény", - "browse-collections": "Gyűjtemények böngészése", - "more-in-genre": "Több hasonló {0}", - "browse-more-in-genre": "Böngéssz a {0}", - "recently-updated": "Nemrég frissítve", - "browse-recently-updated": "Nemrég frissítve böngészése", - "smart-filters": "Intelligens szűrők", - "external-sources": "Külső források", - "browse-external-sources": "Külső források böngészése", - "browse-smart-filters": "Intelligens szűrők böngészése", - "search": "Keresés", - "smart-filter-doesnt-exist": "Az intelligens szűrő nem létezik", - "external-source-already-exists": "A külső forrás már létezik", - "external-source-required": "ApiKey és Host szükséges", - "external-source-doesnt-exist": "A külső forrás nem létezik", - "unable-to-reset-k+": "Nem lehet a licenszt visszaállítani egy hiba miatt. Kérlek fordulj a Kavita+ Supporthoz", - "theme-doesnt-exist": "Téma fájl hiányzik vagy hibás", - "user-no-access-library-from-series": "Felhasználónak nincs hozzáférése a könyvtárhoz amihez ez a sorozat tartozik", - "reading-list-name-exists": "Egy hasonló nevű olvasási lista már létezik", - "cleanup": "Takarítás", - "check-updates": "Frissítések ellenőrzése", - "license-check": "Licensz ellenőrzése", - "chapter-num": "Fejezet {0}", - "issue-num": "Kiadás {0}{1}", - "book-num": "Könyv {0}", - "volume-num": "Kötet {0}", - "series-restricted-age-restriction": "A felhasználó nem nézheti meg ezt a sorozatot korhatár miatt", - "update-yearly-stats": "Éves statisztika frissítése", - "backup": "Mentés", - "kavita+-data-refresh": "Kavita+ adat frissítés", - "scan-libraries": "Könyvtárak szkennelése", - "remove-from-want-to-read": "Olvasandó tisztítása", - "browse-reading-lists": "Olvasási Listák böngészése", - "external-source-already-in-use": "Van egy létező folyamat ezzel a külső forrással", - "not-authenticated": "A felhasználó nincs azonosítva", - "unable-to-register-k+": "Nem lehet a licenszt regisztrálni egy hiba miatt. Kérlek fordulj a Kavita+ Supporthoz", - "anilist-cred-expired": "AniList azonosító lejárt vagy nincs beállítva", - "reading-list-restricted": "Az olvasási lista nem létezik, vagy nincs hozzá hozzáférésed", - "query-required": "Meg kell adnod egy lekérdezés paramétert", - "search-description": "Sorozat, gyűjtemény vagy olvasási lista keresése", - "favicon-doesnt-exist": "Favikon nem létezik", - "epub-malformed": "A fájl tartalma hibás! Nem lehet olvasni.", - "collection-tag-title-required": "A gyűjtemény címe nem lehet üres", - "reading-list-title-required": "Az olvasási lista címe nem lehet üres", - "collection-tag-duplicate": "Egy azonos nevű gyűjtemény már létezik", - "device-duplicate": "Egy azonos nevű eszköz már létezik", - "device-not-created": "Ez az eszköz még nem létezik. Kérlek készítsd el először", - "smart-filter-already-in-use": "Van egy létező adatfolyam ezzel az Okos Szűrővel", - "dashboard-stream-doesnt-exist": "Az Irányítópult Adatfolyam nem létezik", - "sidenav-stream-doesnt-exist": "OldalNav Adatfolyam nem létezik", - "scrobble-bad-payload": "Rossz adat a Feldolgozó szolgáltatótól", - "bad-copy-files-for-download": "Nem lehet fájlokat másolni az átmeneti könyvtár archívum letöltésébe.", - "generic-create-temp-archive": "Probléma volt az átmeneti archívum létrehozásakor", - "epub-html-missing": "Nem lehet megfelelő html-t találni ahhoz az oldalhoz", - "send-to-permission": "Nem lehet nem-EPUB vagy PDF-et az eszközökre, mivel a Kindle nem támogatja", - "progress-must-exist": "A folyamatnak léteznie kell a felhasználón", - "report-stats": "Kimutatás Statisztikák", - "check-scrobbling-tokens": "Ellenőrizd a Feldolgozó tokeneket", - "process-scrobbling-events": "Feldolgozó események feldolgozása", - "process-processed-scrobbling-events": "A feldolgozott Feldolgozó események felolgozása", - "generic-cover-volume-save": "Nem lehet borítóképet menteni a kötethez", - "person-doesnt-exist": "A személy nem létezik", - "person-name-required": "A személy neve kötelező, és nem lehet üres", - "email-taken": "Az e-mail már használatban van", - "person-name-unique": "A személy nevének egyedinek kell lennie" -} diff --git a/API/I18N/id.json b/API/I18N/id.json index b38fabefb..933ed0154 100644 --- a/API/I18N/id.json +++ b/API/I18N/id.json @@ -1,104 +1,8 @@ { - "invalid-password": "Kata Sandi Salah", + "invalid-password": "Sandi Tidak Sah", "validate-email": "Terdapat masalah saat memvalidasi email kamu: {0}", "confirm-email": "Kamu harus mengonfirmasi email kamu terlebih dahulu", "disabled-account": "Akunmu dinonaktifkan. Hubungi admin server.", "denied": "Tidak diizinkan", - "register-user": "Terjadi kesalahan ketika mendaftarkan pengguna", - "generate-token": "Terdapat masalah saat mendapatkan token konfirmasi email. Lihat catatan", - "invalid-email-confirmation": "Konfirmasi email salah", - "age-restriction-update": "Terjadi kesalahan saat mengganti batasan usia", - "not-accessible": "Server anda tidak dapat diakses secara eksternal", - "collections": "Semua koleksi", - "email-sent": "Email terkirim", - "user-already-confirmed": "Pengguna telah dikonfirmasi", - "invalid-token": "Token Salah", - "generic-user-email-update": "Tidak bisa memperbarui email pengguna. Lihat catatan.", - "password-updated": "Kata Sandi telah diubah", - "password-required": "Masukkan kata sandi yang kamu miliki untuk menggantinya kecuali kamu adalah admin", - "invalid-email": "Terdapat kesalahan email pada dokumen pengguna. Lihat catatan untuk tautan apa pun.", - "user-already-invited": "Pengguna telah diundang melalui email ini namun belum menerimanya.", - "not-accessible-password": "Server tidak dapat diakses. Tautan untuk mengatur ulang kata sandi dapat ditemukan di catatan", - "user-already-registered": "Pengguna telah terdaftar sebagai {0}", - "access-denied": "Akses ditolak", - "reading-lists": "Daftar bacaan", - "username-taken": "Nama pengguna sudah ada", - "forgot-password-generic": "Email akan dikirimkan apabila ditemukan di database kami", - "no-user": "Pengguna tidak ditemukan", - "generic-invite-user": "Terdapat masalah saat mengundang pengguna ini. Tolong lihat catatan.", - "permission-denied": "Kamu tidak diizinkan untuk melakukan ini", - "locked-out": "Akses anda dikunci sementara dikarenakan terlalu banyak melakukan otoriasi. Mohon tunggu 10 menit.", - "confirm-token-gen": "Terjadi kesalahan saat membuat token konfirmasi", - "unable-to-reset-key": "Terjadi kesalahan, tidak dapat menyetel ulang kunci", - "invalid-payload": "Muatan tidak valid", - "nothing-to-do": "Tidak ada yang perlu dilakukan", - "share-multiple-emails": "Anda tidak dapat berbagi email ke banyak akun", - "generic-user-update": "Terjadi kesalahan saat memperbaharui pengguna", - "manual-setup-fail": "Penyiapan manual tidak dapat diselesaikan. Harap batalkan dan buat ulang undangannya", - "generic-password-update": "Terjadi kesalahan saat mengkonfirmasi password baru", - "collection-updated": "Kumpulan telah diubah", - "collection-deleted": "Kumpulan telah dihapus", - "device-doesnt-exist": "Perangkat tidak tersedia", - "generic-error": "Terjadi kesalahan, coba lagi", - "collection-doesnt-exist": "Kumpulan tidak tersedia", - "pdf-doesnt-exist": "PDF tidak ada padahal seharusnya ada", - "bookmark-permission": "Anda tidak memiliki izin untuk menandai/membatalkan penanda", - "generic-clear-bookmarks": "Tidak dapat menghapus penanda buku", - "generic-read-progress": "Terjadi masalah saat menyimpan progres", - "reading-list-updated": "Telah diubah", - "perform-scan": "Mohon lakukan pemindaian pada seri atau pustaka ini dan coba lagi", - "series-restricted": "Pengguna tidak memiliki akses ke Seri ini", - "generic-scrobble-hold": "Terjadi kesalahan saat menambahkan pemesanan", - "job-already-running": "Pekerjaan sudah berjalan", - "generic-relationship": "Ada masalah dalam memperbarui hubungan", - "ip-address-invalid": "Alamat IP '{0}' tidak valid", - "bookmark-dir-permissions": "Izin Direktori Penanda Buku tidak benar untuk digunakan oleh Kavita", - "stats-permission-denied": "Anda tidak diizinkan untuk melihat statistik pengguna lain", - "url-required": "Untuk menggunakan, Anda harus menyertakan URL", - "generic-cover-series-save": "Tidak dapat menyimpan gambar sampul ke Seri", - "url-not-valid": "URL tidak mengembalikan gambar yang valid atau memerlukan otorisasi", - "generic-cover-library-save": "Tidak dapat menyimpan gambar sampul ke Pustaka", - "generic-cover-chapter-save": "Tidak dapat menyimpan gambar sampul ke Bab", - "reset-chapter-lock": "Tidak dapat mereset kunci sampul untuk Bab", - "no-image-for-page": "Gambar untuk halaman {0} tidak ditemukan. Coba perbarui halaman untuk memperbaharui cache.", - "encode-as-warning": "Anda tidak dapat mengonversi ke PNG. Untuk sampul, gunakan Perbarui Sampul. Penanda dan favicon tidak dapat diubah kembali.", - "total-logs": "Total log harus diantara 1 dan 30", - "total-backups": "Total backup harus diantara 1 dan 30", - "user-migration-needed": "Pengguna ini perlu migrasi. Minta pengguna tersebut log out dan login kembali untuk memicu alur migrasi", - "generic-invite-email": "Terdapat masalah saat mengirim ulang undangan email", - "admin-already-exists": "Admin sudah ada", - "invalid-username": "Nama pengguna salah", - "critical-email-migration": "Terjadi kesalahan saat migrasi email. Hubungi bantuan", - "chapter-doesnt-exist": "Bab tidak ada", - "file-missing": "Berkas tidak ditemukan didalam buku", - "invalid-access": "Akses tidak valid", - "bookmark-save": "Tidak dapat menyimpan penanda buku", - "cache-file-find": "Tidak dapat menemukan gambar yang telah disimpan dalam penyimpanan sementara. Muat ulang dan coba lagi.", - "name-required": "Nama tidak boleh kosong", - "valid-number": "Nomor halaman harus valid", - "duplicate-bookmark": "Entri penanda buku yang sama sudah ada", - "reading-list-permission": "Anda tidak memiliki izin pada daftar bacaan ini atau daftar tersebut tidak ada", - "reading-list-position": "Tidak dapat memperbarui posisi", - "reading-list-item-delete": "Tidak dapat menghapus item", - "reading-list-deleted": "Daftar Bacaan telah dihapus", - "generic-reading-list-delete": "Terjadi kesalahan saat menghapus daftar bacaan", - "generic-reading-list-update": "Terjadi kesalahan saat mengubah daftar bacaan", - "generic-reading-list-create": "Terjadi kesalahan saat membuat daftar bacaan", - "reading-list-doesnt-exist": "Daftar bacaan tidak ada", - "libraries-restricted": "Pengguna tidak memiliki akses ke pustaka manapun", - "no-series": "Gagal mengambil seri untuk pustaka ini", - "no-series-collection": "Gagal mengambil seri untuk Koleksi", - "generic-series-delete": "Terjadi kesalahan saat menghapus seri", - "generic-series-update": "Terjadi kesalahan saat mengubah seri", - "series-updated": "Berhasil diubah", - "update-metadata-fail": "Tidak dapat mengubah metadata", - "age-restriction-not-applicable": "Tidak ada batasan", - "generic-cover-collection-save": "Tidak dapat menyimpan gambar sampul ke Koleksi", - "generic-cover-reading-list-save": "Tidak dapat menyimpan gambar sampul ke Daftar Baca", - "generic-user-delete": "Tidak dapat menghapus pengguna", - "email-not-enabled": "Email tidak diaktifkan pada server ini. Anda tidak dapat melakukan tindakan ini.", - "generic-device-update": "Terjadi error saat memperbarui perangkat", - "generic-device-create": "Ada error saat membuat perangkat", - "generic-device-delete": "Terjadi error saat menghapus perangkat", - "greater-0": "{0} harus lebih besar dari 0" + "register-user": "Terjadi kesalahan ketika mendaftarkan pengguna" } diff --git a/API/I18N/it.json b/API/I18N/it.json index cf43101a6..e6022d83d 100644 --- a/API/I18N/it.json +++ b/API/I18N/it.json @@ -53,6 +53,7 @@ "ip-address-invalid": "Indirizzo IP '{0}' non valido", "bookmark-dir-permissions": "La directory dei segnalibri non dispone delle autorizzazioni corrette per l'utilizzo da parte di Kavita", "confirm-email": "Prima devi confermare la tua email", + "bad-credentials": "Le tue credenziali non sono corrette", "register-user": "Qualcosa è andato storto nella registrazione dell'utente", "confirm-token-gen": "Si è verificato un problema durante la generazione di un token di conferma", "generic-user-email-update": "Impossibile aggiornare l'e-mail per l'utente. Controlla i log.", @@ -90,7 +91,7 @@ "user-no-access-library-from-series": "L'utente non ha accesso alla libreria a cui appartiene questa serie", "volume-num": "Volume {0}", "book-num": "Libro {0}", - "issue-num": "Numero {0}{1}", + "issue-num": "Problema {0}{1}", "chapter-num": "Capitolo {0}", "epub-malformed": "Il file è corrotto! Non posso leggere.", "collection-updated": "Collezione aggiornata con successo", @@ -120,7 +121,7 @@ "generic-device-update": "Si è verificato un errore durante l'aggiornamento del dispositivo", "generic-device-delete": "Si è verificato un errore durante l'eliminazione del dispositivo", "greater-0": "{0} deve essere maggiore di 0", - "send-to-kavita-email": "Invia al dispositivo non può essere utilizzato senza la configurazione dell'email", + "send-to-kavita-email": "Invia al dispositivo non può essere utilizzato con il servizio e-mail di Kavita. Si prega di configurare il proprio.", "generic-send-to": "Si è verificato un errore durante l'invio dei file al dispositivo", "generic-favicon": "Si è verificato un problema durante il recupero della favicon per il dominio", "library-name-exists": "Il nome della libreria esiste già. Scegli un nome univoco per il server.", @@ -158,50 +159,5 @@ "reading-list-restricted": "L'elenco di lettura non esiste o non hai accesso", "browse-want-to-read": "Sfoglia Vuoi leggere", "want-to-read": "Vuoi leggere", - "collection-deleted": "Collezione cancellata", - "browse-external-sources": "Sfoglia Sorgenti Esterne", - "smart-filters": "Filtri Intelligenti", - "browse-smart-filters": "Sfoglia per Filtri Intelligenti", - "smart-filter-doesnt-exist": "Il Filtro intelligente non esiste", - "invalid-email": "La mail nel file per l'utente non è un email valida. Vedi i log per i link.", - "external-source-already-exists": "La Sorgente Esterna esiste già", - "external-source-doesnt-exist": "La Sorgente Esterna non esiste", - "external-sources": "Sorgenti Esterne", - "external-source-required": "ApiKey e Host sono obbligatori", - "external-source-already-in-use": "Esiste uno stream esistente con questa Sorgente Esterna", - "smart-filter-already-in-use": "Esiste uno stream esistente con questo Filtro Intelligente", - "dashboard-stream-doesnt-exist": "Dashboard Stream non esiste", - "sidenav-stream-doesnt-exist": "SideNav Stream non esiste", - "browse-more-in-genre": "Sfoglia di più in {0}", - "more-in-genre": "Altro in Genere {0}", - "recently-updated": "Aggiornato di recente", - "browse-recently-updated": "Sfoglia gli aggiornamenti recenti", - "email-not-enabled": "L'email non è attivata in questo server. Non puoi compiere questa azione.", - "send-to-unallowed": "Non puoi inviare ad un dispositivo non tuo", - "send-to-size-limit": "Il/I file che stai cercando di mandare sono troppo grandi per il tuo provider email", - "unable-to-reset-k+": "Impossibile ripristinare la licenza Kavita+. Contatta il supporto Kavita+", - "check-updates": "Controlla aggiornamenti", - "license-check": "Controlla Licenza", - "process-scrobbling-events": "Elabora gli eventi di scrobbling", - "cleanup": "Pulizia", - "report-stats": "Rapporto Statistiche", - "check-scrobbling-tokens": "Controlla i Tokens per lo Scrobbling", - "process-processed-scrobbling-events": "Elabora gli eventi di scrobbling processati", - "remove-from-want-to-read": "Voglio per Pulizia Letture", - "scan-libraries": "Scansiona librerie", - "update-yearly-stats": "Aggiorna le statistiche annuali", - "kavita+-data-refresh": "Aggiornamento dati Kavita+", - "backup": "Backup", - "account-email-invalid": "L'e-mail in archivio per l'account amministratore non è un'e-mail valida. Impossibile inviare e-mail di prova.", - "email-settings-invalid": "Informazioni mancanti nelle impoastazioni email. Assicurati che tutte le impostazioni email siano salvate.", - "person-doesnt-exist": "La persona non esiste", - "email-taken": "Email già in uso", - "person-name-required": "Il nome della persona è richiesto e non può essere null", - "person-name-unique": "Il nome della persona deve essere univoco", - "person-image-doesnt-exist": "La persona non esiste su CoversDB", - "collection-already-exists": "La collezione esiste già", - "generic-cover-volume-save": "Impossibile salvare l'immagine di copertina nel Volume", - "generic-cover-person-save": "Impossibile salvare l'immagine di copertina nella Persona", - "error-import-stack": "Si è verificato un errore durante l'importazione dello stack MAL", - "kavitaplus-restricted": "Riservato a Kavita+" + "collection-deleted": "Collezione cancellata" } diff --git a/API/I18N/ja.json b/API/I18N/ja.json index 07efb40ef..2256184fb 100644 --- a/API/I18N/ja.json +++ b/API/I18N/ja.json @@ -3,6 +3,7 @@ "invalid-token": "無効トークン", "invalid-password": "無効なパスワード", "validate-email": "メールの検証中に問題が発生しました: {0}", + "bad-credentials": "資格情報が正しくありません", "confirm-email": "最初にメールを確認する必要があります", "disabled-account": "あなたのアカウントは無効になりました。サーバー管理者に連絡してください。", "locked-out": "認証試行が多すぎるとロックアウトされました。 10分ほどお待ちください。", @@ -14,182 +15,5 @@ "denied": "禁じられている", "register-user": "ユーザー登録時に問題が発生しました", "nothing-to-do": "何もすることがない", - "permission-denied": "この操作は許可されていません", - "password-updated": "パスワードは更新されました", - "user-already-registered": "ユーザー({0})は既に登録されています", - "no-user": "ユーザーが存在しません", - "generate-token": "確認メールトークンの生成に問題がありました。ログを見てください", - "invalid-email-confirmation": "無効な電子メール確認", - "admin-already-exists": "管理者は既に存在しています", - "age-restriction-update": "年齢制限の更新エラーが発生しました", - "not-accessible": "サーバーが外部からアクセスできません", - "email-sent": "Eメールの送信", - "generic-password-update": "新しいパスワードの確認時に予期しないエラーが発生しました", - "user-already-confirmed": "ユーザーは確認済みです", - "user-migration-needed": "このユーザーは移行する必要があります。移行フローを実行するために、ログアウトしてからログインしてください", - "generic-user-update": "ユーザーの更新時に例外が発生しました", - "generic-user-email-update": "ユーザーの電子メールを更新できません。ログを確認してください。", - "collection-updated": "コレクションは正常に更新されました", - "critical-email-migration": "メール移行中に問題が発生しました。サポートに連絡してください", - "invalid-email": "ユーザーのEメールが有効なEメールではありません。リンクについてはログを参照してください。", - "user-already-invited": "ユーザーはすでにメールで招待されており、まだ招待を受け入れていません。", - "collection-doesnt-exist": "コレクションは存在しません", - "chapter-doesnt-exist": "章が存在しません", - "not-accessible-password": "サーバーにアクセスできません。パスワードをリセットするためのリンクは過去ログにあります", - "invalid-username": "無効なユーザー名", - "generic-invite-email": "招待メールの再送信に問題が発生しました", - "generic-error": "何か問題が発生しました", - "file-missing": "ブックにファイルが見つかりません", - "username-taken": "ユーザー名はすでに使われています", - "manual-setup-fail": "手動セットアップが完了できません。キャンセルして招待を作り直してください", - "forgot-password-generic": "データベースに登録されているメールアドレスにメールが送信されます", - "generic-invite-user": "ユーザーを招待する際に問題が発生しました。ログを確認してください。", - "collection-deleted": "コレクションを削除", - "device-doesnt-exist": "デバイスが存在しません", - "generic-device-create": "デバイスを作成するときにエラーがありました", - "generic-device-update": "デバイスを更新する際にエラーがありました", - "generic-device-delete": "デバイスを削除した際にエラーがありました", - "greater-0": "{0} は 0 よりも大きいです", - "send-to-kavita-email": "メールの設定がないと、デバイスへの送信は使用できません", - "send-to-device-status": "ファイルをデバイスに転送する", - "generic-send-to": "ファイルをデバイスに送信する際にエラーが発生しました", - "email-not-enabled": "メールがこのサーバーでは有効になっていません。このアクションは実行することができません.", - "send-to-unallowed": "あなたものでないデバイスには送信できません", - "bookmarks-empty": "ブックマークを空にすることはできません", - "delete-library-while-scan": "スキャンが進行中の間はライブラリを削除することはできません。スキャンが完了するのを待つか、Kavitaを再起動してから削除を試みてください.", - "perform-scan": "このシリーズもしくはライブラリのスキャンを実行して再試行してください", - "generic-read-progress": "進捗の保存中に問題が発生しました", - "cache-file-find": "キャッシュイメージが見つかりませんでした。リロードしてもう一度試してください.", - "name-required": "空白の名前はつけることができません", - "reading-list-item-delete": "アイテムを削除できませんでした", - "reading-list-deleted": "リーディングリストは削除されました", - "generic-reading-list-delete": "リーディングリストを削除している際に問題が発生しました。", - "generic-reading-list-update": "リーディングリストを更新している際に問題が発生しました", - "no-series-collection": "コレクションのシリーズを取得できませんでした", - "generic-series-delete": "シリーズの削除中に問題が発生しました", - "generic-series-update": "シリーズの更新中にエラーが発生しました。", - "series-updated": "アップロードが成功しました", - "total-backups": "総バックアップ数は1から30の間でなければなりません", - "url-required": "使用するにはURLを渡す必要があります", - "generic-cover-series-save": "シリーズにカバー画像を保存できません", - "user-doesnt-exist": "ユーザーが存在しません", - "generic-library": "重大な問題が発生しました。もう一度試してください.", - "reading-list-permission": "リーディングリストを閲覧する権限がないかリストが存在しません", - "reading-list-position": "位置が更新できませんでした", - "reading-list-updated": "アップデート済", - "reading-list-doesnt-exist": "リーディングリストが存在しません", - "generic-scrobble-hold": "保留の追加時にエラーが発生しました。", - "no-series": "ライブラリのシリーズを取得できませんでした。", - "series-restricted": "このシリーズに対するアクセス権がありません", - "generic-relationship": "関係の更新中に問題が発生しました", - "ip-address-invalid": "IPアドレス '{0}' は無効です", - "bookmark-dir-permissions": "ブックマークディレクトリには、Kavitaが使用するための正しい権限がありません", - "url-not-valid": "URLは有効な画像を返さないか、認証が必要です", - "volume-doesnt-exist": "巻が存在しません", - "library-name-exists": "そのライブラリ名はすでに存在します。一意な名前をサーバーで選択してください.", - "invalid-path": "無効なパス", - "generic-library-update": "ライブラリを更新中に重大な問題が発生しました.", - "invalid-access": "アクセスが無効です", - "no-image-for-page": "ページ{0}の画像が存在しません。再キャッシュを許可するためにリフレッシュを試してみてください。", - "generic-clear-bookmarks": "ブックマークをクリアできませんでした", - "bookmark-permission": "ブックマーク/ブックマークを解除する権限があなたにはありません。", - "valid-number": "有効なページ番号である必要があります", - "duplicate-bookmark": "重複したブックマークエントリーがすでに存在します", - "send-to-size-limit": "送信しようとしているファイルはあなたのメールプロバイダにとっては大きすぎます。", - "series-doesnt-exist": "シリーズが存在しません", - "pdf-doesnt-exist": "PDFが存在すべきですが存在しません。", - "generic-reading-list-create": "リーディングリストを作成している際に問題が発生しました", - "libraries-restricted": "ライブラリに対してのアクセス権がありません", - "job-already-running": "ジョブはすでに実行されています。", - "encode-as-warning": "PNGに変換することはできません。カバーの場合は「Refresh Covers」を使用してください。ブックマークとファビコンは元に戻すことはできません。", - "total-logs": "総ログ数は1から30の間でなければなりません", - "stats-permission-denied": "他のユーザーの統計を表示する権限がありません", - "bookmark-doesnt-exist": "ブックマークが存在しません", - "must-be-defined": "{0} を定義する必要があります。", - "generic-favicon": "ドメインのファビコンを取得する際に問題が発生しました。", - "invalid-filename": "無効なファイル名", - "file-doesnt-exist": "ファイルが存在しません", - "update-metadata-fail": "メタデータがアップロードできませんでした。", - "age-restriction-not-applicable": "無制限", - "no-cover-image": "カバー画像がありません", - "no-library-access": "ユーザーはこのライブラリのアクセス権がありません。", - "library-doesnt-exist": "ライブラリが存在しません。", - "bookmark-save": "ブックマークを保存できませんでした", - "smart-filter-already-in-use": "このスマートフィルターに対応する既存のストリームがあります", - "generic-cover-collection-save": "コレクションへのカバー画像の保存ができません", - "generic-cover-reading-list-save": "読書リストにカバー画像を保存できません", - "generic-cover-chapter-save": "チャプターへのカバー画像の保存ができません。", - "collections": "すべてのコレクション", - "browse-recently-added": "最近追加されたものを表示", - "browse-collections": "コレクションによるブラウズ", - "search-description": "シリーズ、コレクション、または読書リストを検索します", - "external-source-already-exists": "外部ソースは既に存在する", - "external-source-required": "ApiKeyとホストが必要", - "external-source-doesnt-exist": "外部ソースが存在しません", - "external-source-already-in-use": "この外部ソースで既存のストリームがあります", - "epub-malformed": "ファイルが不正です。読み取ることができません。", - "epub-html-missing": "そのページに適したHTMLが見つかりませんでした", - "device-duplicate": "このデバイス名はすでに存在しています", - "reading-list-name-exists": "この名前の読書リストはすでに存在しています", - "browse-more-in-genre": "もっと見る {0}", - "recently-updated": "最近更新", - "browse-recently-updated": "最近のアップロードを見る", - "smart-filters": "スマートフィルター", - "browse-external-sources": "外部ソースを参照", - "browse-smart-filters": "スマートフィルタでブラウズ", - "reading-list-restricted": "読書リストは存在しませんか、アクセスがない", - "search": "検索", - "dashboard-stream-doesnt-exist": "Dashboard Stream は存在しません", - "sidenav-stream-doesnt-exist": "SideNav Stream は存在しません", - "reading-list-title-required": "読書リスト タイトルは空にすることはできません", - "progress-must-exist": "進めるにはユーザーが存在する必要があります", - "send-to-permission": "Kindleでは、EPUBやPDF以外の形式はサポートされていないため、デバイスに送信できません", - "volume-num": "ボリューム {0}", - "book-num": "本 {0}", - "issue-num": "問題 {0}{1}", - "reset-chapter-lock": "チャプター用のカバーロックをリセットできない", - "on-deck": "デッキ", - "browse-on-deck": "デッキで見る", - "reading-lists": "リーディングリスト", - "browse-reading-lists": "読書リストで閲覧", - "more-in-genre": "さらなるジャンル {0}", - "external-sources": "外部ソース", - "smart-filter-doesnt-exist": "スマートフィルタは存在しません", - "theme-doesnt-exist": "テーマファイル欠落または無効", - "bad-copy-files-for-download": "ファイルをtempディレクトリのアーカイブのダウンロードにコピーできません.", - "unable-to-reset-k+": "エラーによるKavita +ライセンスをリセットできません。 Kavita+サポートへのアクセス", - "collection-tag-title-required": "コレクションタイトルは空にすることはできません", - "device-not-created": "このデバイスはまだ存在しません。 まずは作成しましょう", - "want-to-read": "読みたい", - "browse-want-to-read": "読みたいを表示", - "query-required": "クエリパラメータを渡す必要があります", - "not-authenticated": "ユーザが認証されていない", - "anilist-cred-expired": "AniList 認証が期限切れまたは設定されていない", - "favicon-doesnt-exist": "ファビコンは存在しません", - "unable-to-register-k+": "エラーによりライセンスを登録できません。 Kavita+サポートへのアクセス", - "series-restricted-age-restriction": "年齢制限により、ユーザーはこのシリーズを表示することが許可されていません", - "generic-cover-library-save": "ライブラリへのカバー画像の保存ができません。", - "access-denied": "アクセスがない", - "generic-user-delete": "ユーザを削除できません", - "generic-user-pref": "問題の保存設定があった", - "opds-disabled": "このサーバではOPDSが有効になっていない", - "recently-added": "最近追加", - "libraries": "すべてのライブラリ", - "browse-libraries": "ライブラリでブラウズする", - "scrobble-bad-payload": "Scrobbleプロバイダからの悪いペイロード", - "generic-create-temp-archive": "一時アーカイブの作成中に問題が発生しました", - "user-no-access-library-from-series": "ユーザーは、このシリーズが所属するライブラリにアクセス権限がありません", - "collection-tag-duplicate": "この名前のコレクションは既に存在しています", - "account-email-invalid": "管理者アカウントに登録されている電子メールは有効な電子メールではありません。 テストメールを送信できません。", - "check-updates": "アップデートをチェックする", - "license-check": "ライセンスを確認", - "collection-already-exists": "コレクションは既に存在しています", - "email-settings-invalid": "メール設定に不足している情報があります。すべてのメール設定が保存されていることを確認してください。", - "email-taken": "メールアドレスは既に使われています", - "person-doesnt-exist": "人物は存在しません", - "person-name-unique": "人名は一意でなければなりません", - "person-name-required": "人物の名前は必須であり、空にすることはできません", - "person-image-doesnt-exist": "人物はCoversDBに存在しません", - "generic-cover-person-save": "カバー画像を人物に保存できません", - "generic-cover-volume-save": "カバー画像を巻に保存できません" + "permission-denied": "この操作は許可されていません" } diff --git a/API/I18N/as.json b/API/I18N/kn.json similarity index 100% rename from API/I18N/as.json rename to API/I18N/kn.json diff --git a/API/I18N/ko.json b/API/I18N/ko.json index 7fec9f60a..66be3300e 100644 --- a/API/I18N/ko.json +++ b/API/I18N/ko.json @@ -1,213 +1,175 @@ { "confirm-email": "먼저 이메일을 확인해야 합니다", - "locked-out": "인증 시도가 너무 많아 계정이 잠겼습니다. 10분 후에 다시 시도하세요.", - "invalid-password": "잘못된 비밀번호입니다", - "user-already-registered": "사용자는 이미 {0}(으)로 등록되어 있습니다", - "password-updated": "비밀번호가 업데이트되었습니다", - "not-accessible-password": "서버에 접근할 수 없습니다. 비밀번호 재설정 링크는 로그에 있습니다", - "not-accessible": "서버가 외부에서 접근할 수 없습니다", + "bad-credentials": "자격 증명이 올바르지 않습니다", + "locked-out": "너무 많은 인증 시도로 인해 잠겼습니다. 10분 동안 기다려 주십시오.", + "invalid-password": "유효하지 않은 비밀번호", + "user-already-registered": "사용자는 이미 {0}로 등록되어 있습니다", + "password-updated": "비밀번호 업데이트됨", + "not-accessible-password": "서버에 액세스할 수 없습니다. 비밀번호 재설정 링크는 로그에 있습니다", + "not-accessible": "외부에서 서버에 액세스할 수 없습니다", "chapter-doesnt-exist": "챕터가 존재하지 않습니다", "file-missing": "책에서 파일을 찾을 수 없습니다", - "generic-error": "문제가 발생했습니다. 다시 시도해주세요", - "generic-device-delete": "장치 삭제 중 오류가 발생했습니다", - "greater-0": "{0}은(는) 0보다 커야 합니다", - "send-to-device-status": "장치로 파일을 전송 중입니다", - "generic-send-to": "장치로 파일 전송 중 오류가 발생했습니다", + "generic-error": "문제가 발생했습니다, 다시 시도하십시오", + "generic-device-delete": "장치를 삭제하는 중에 오류가 발생했습니다", + "greater-0": "{0}는 0보다 커야 합니다", + "send-to-device-status": "장치로 파일 전송", + "generic-send-to": "파일을 장치로 보내는 중 오류가 발생했습니다", "volume-doesnt-exist": "볼륨이 존재하지 않습니다", - "generic-favicon": "도메인의 파비콘 가져오기 중 문제가 발생했습니다", - "no-library-access": "사용자는 이 라이브러리에 접근할 수 없습니다", + "generic-favicon": "도메인의 파비콘을 가져오는 중에 문제가 발생했습니다", + "no-library-access": "사용자는 이 라이브러리에 액세스할 수 없습니다", "user-doesnt-exist": "사용자가 존재하지 않습니다", "library-doesnt-exist": "라이브러리가 존재하지 않습니다", "duplicate-bookmark": "중복된 북마크 항목이 이미 존재합니다", "reading-list-position": "위치를 업데이트할 수 없습니다", - "reading-list-deleted": "독서 목록이 삭제되었습니다", - "generic-reading-list-delete": "독서 목록 삭제 중 문제가 발생했습니다", - "reading-list-doesnt-exist": "독서 목록이 존재하지 않습니다", + "reading-list-deleted": "읽기 목록이 삭제되었습니다", + "generic-reading-list-delete": "읽기 목록을 삭제하는 중에 문제가 발생했습니다", + "reading-list-doesnt-exist": "읽기 목록이 존재하지 않습니다", "no-series": "라이브러리에 대한 시리즈를 가져올 수 없습니다", "age-restriction-not-applicable": "제한 없음", - "generic-relationship": "관계 업데이트 중 문제가 발생했습니다", - "job-already-running": "작업이 이미 실행 중입니다", + "generic-relationship": "관계를 업데이트하는 중에 문제가 발생했습니다", + "job-already-running": "이미 실행 중인 작업", "url-required": "사용할 URL을 전달해야 합니다", - "reading-list-title-required": "독서 목록 제목은 비워둘 수 없습니다", + "reading-list-title-required": "읽기 목록 제목은 비워둘 수 없습니다", "progress-must-exist": "사용자에게 진행 상황이 있어야 합니다", "volume-num": "볼륨 {0}", "chapter-num": "챕터 {0}", "disabled-account": "계정이 비활성화되었습니다. 서버 관리자에게 문의하세요.", - "validate-email": "이메일 확인 중 문제가 발생했습니다: {0}", - "register-user": "사용자 등록 중에 문제가 발생했습니다", - "confirm-token-gen": "확인 토큰 생성 중 문제가 발생했습니다", - "permission-denied": "이 작업을 수행할 권한이 없습니다", - "denied": "허용되지 않습니다", + "validate-email": "이메일을 확인하는 중에 문제가 발생했습니다: {0}", + "register-user": "사용자를 등록하는 중에 문제가 발생했습니다", + "confirm-token-gen": "확인 토큰을 생성하는 중에 문제가 발생했습니다", + "permission-denied": "이 작업을 수행할 수 없습니다", + "denied": "허용되지 않음", "password-required": "관리자가 아닌 경우 계정을 변경하려면 기존 비밀번호를 입력해야 합니다", - "invalid-payload": "잘못된 페이로드입니다", - "nothing-to-do": "할 일이 없습니다", - "share-multiple-emails": "여러 계정에 걸쳐 이메일을 공유할 수 없습니다", - "invalid-token": "잘못된 토큰입니다", + "invalid-payload": "유효하지 않은 페이로드", + "nothing-to-do": "할 것이 없음", + "share-multiple-emails": "여러 계정에서 이메일을 공유할 수 없습니다", + "invalid-token": "유효하지 않은 토큰", "unable-to-reset-key": "문제가 발생하여 키를 재설정할 수 없습니다", - "generate-token": "확인 이메일 토큰 생성 중 문제가 발생했습니다. 로그를 확인하세요", + "generate-token": "확인 이메일 토큰을 생성하는 중에 문제가 발생했습니다. 로그 보기", "no-user": "사용자가 존재하지 않습니다", - "age-restriction-update": "연령 제한 업데이트 중 오류가 발생했습니다", - "username-taken": "이미 사용 중인 사용자 이름입니다", + "age-restriction-update": "연령 제한을 업데이트 하는 중에 오류가 발생했습니다", + "username-taken": "이미 사용중인 이름입니다", "user-already-confirmed": "사용자가 이미 확인되었습니다", - "generic-user-update": "사용자 업데이트 중 예외가 발생했습니다", - "manual-setup-fail": "수동 설정을 완료할 수 없습니다. 초대를 취소하고 다시 생성하세요", + "generic-user-update": "사용자를 업데이트 중에 예외가 발생했습니다", + "manual-setup-fail": "수동 설정을 완료할 수 없습니다. 초대를 취소하고 다시 만드십시오", "user-already-invited": "사용자는 이미 이 이메일로 초대되었으며 아직 초대를 수락하지 않았습니다.", - "generic-invite-user": "사용자 초대 중 문제가 발생했습니다. 로그를 확인하세요.", - "invalid-email-confirmation": "잘못된 이메일 확인입니다", - "generic-user-email-update": "사용자의 이메일을 업데이트할 수 없습니다. 로그를 확인하세요.", - "generic-password-update": "새 비밀번호 확인 중 예기치 않은 오류가 발생했습니다", - "email-sent": "이메일이 발송되었습니다", + "generic-invite-user": "사용자를 초대하는 중에 문제가 발생했습니다. 로그를 확인하십시오.", + "invalid-email-confirmation": "잘못된 이메일 확인", + "generic-user-email-update": "사용자의 이메일을 업데이트할 수 없습니다. 로그를 확인하십시오.", + "generic-password-update": "새 비밀번호를 확인하는 중에 예상치 못한 오류가 발생했습니다", + "email-sent": "이메일을 보냈습니다", "admin-already-exists": "관리자가 이미 존재합니다", - "user-migration-needed": "이 사용자는 마이그레이션이 필요합니다. 로그아웃 후 로그인하여 마이그레이션을 진행하세요", - "forgot-password-generic": "해당 이메일이 데이터베이스에 존재하면 이메일이 발송됩니다", - "generic-invite-email": "초대 이메일 재발송 중 문제가 발생했습니다", - "invalid-username": "잘못된 사용자 이름입니다", - "critical-email-migration": "이메일 마이그레이션 중 문제가 발생했습니다. 지원팀에 연락하세요", + "user-migration-needed": "이 사용자는 이전해야 합니다. 로그아웃하고 로그인하여 마이그레이션 흐름을 트리거하도록 합니다", + "forgot-password-generic": "이메일이 데이터베이스에 존재하는 경우 이메일이 이메일로 전송됩니다", + "generic-invite-email": "초대 이메일을 다시 보내는 중에 문제가 발생했습니다", + "invalid-username": "유효하지 않은 아이디", + "critical-email-migration": "이메일 이전 중에 문제가 발생했습니다. 연락처 지원", "collection-updated": "컬렉션이 성공적으로 업데이트되었습니다", "collection-doesnt-exist": "컬렉션이 존재하지 않습니다", - "generic-device-create": "장치 생성 중 오류가 발생했습니다", + "generic-device-create": "장치를 생성하는 중에 오류가 발생했습니다", "device-doesnt-exist": "장치가 존재하지 않습니다", - "generic-device-update": "장치 업데이트 중 오류가 발생했습니다", - "send-to-kavita-email": "이메일 설정 없이 장치로 전송할 수 없습니다", - "no-cover-image": "커버 이미지가 없습니다", + "generic-device-update": "장치를 업데이트 하는 중에 오류가 발생했습니다", + "send-to-kavita-email": "장치로 보내기는 Kavita의 이메일 서비스와 함께 사용할 수 없습니다. 직접 구성하십시오.", + "no-cover-image": "표지 이미지 없음", "series-doesnt-exist": "시리즈가 존재하지 않습니다", "bookmarks-empty": "북마크는 비워둘 수 없습니다", "bookmark-doesnt-exist": "북마크가 존재하지 않습니다", - "must-be-defined": "{0}은(는) 정의되어야 합니다", - "generic-library": "심각한 문제가 발생했습니다. 다시 시도해주세요.", - "invalid-filename": "잘못된 파일 이름입니다", - "file-doesnt-exist": "파일이 존재하지 않습니다", - "library-name-exists": "라이브러리 이름이 이미 존재합니다. 서버에서 고유한 이름을 선택하세요.", - "invalid-path": "잘못된 경로입니다", - "delete-library-while-scan": "스캔이 진행 중일 때는 라이브러리를 삭제할 수 없습니다. 스캔이 완료되거나 Kavita를 재시작한 후 삭제를 시도하세요", - "pdf-doesnt-exist": "존재해야 할 PDF가 없습니다", - "no-image-for-page": "{0} 페이지에 해당하는 이미지가 없습니다. 다시 캐시할 수 있도록 새로 고침하세요.", + "must-be-defined": "{0}을(를) 정의해야 합니다", + "generic-library": "심각한 문제가 있었습니다. 다시 시도해 주세요.", + "invalid-filename": "유효하지 않은 파일 이름", + "file-doesnt-exist": "파일이 없습니다", + "library-name-exists": "라이브러리 이름이 이미 존재합니다. 서버에 고유한 이름을 선택하십시오.", + "invalid-path": "유효하지 않은 경로", + "delete-library-while-scan": "스캔이 진행 중인 동안에는 라이브러리를 삭제할 수 없습니다. 스캔이 완료될 때까지 기다리거나 Kavita를 다시 시작한 다음 삭제를 시도하십시오", + "pdf-doesnt-exist": "PDF가 있어야 할 때 존재하지 않음", + "no-image-for-page": "페이지 {0}에 해당 이미지가 없습니다. 재캐시를 허용하려면 새로고침해 보세요.", "generic-clear-bookmarks": "북마크를 지울 수 없습니다", - "invalid-access": "잘못된 접근입니다", - "generic-library-update": "라이브러리 업데이트 중 심각한 문제가 발생했습니다.", - "bookmark-permission": "북마크 추가/제거 권한이 없습니다", - "perform-scan": "이 시리즈나 라이브러리에 대해 스캔을 수행하고 다시 시도하세요", + "invalid-access": "유효하지 않은 액세스", + "generic-library-update": "라이브러리를 업데이트하는 중 심각한 문제가 발생했습니다.", + "bookmark-permission": "북마크/북마크해제 권한이 없습니다", + "perform-scan": "이 시리즈 또는 라이브러리에서 스캔을 수행하고 다시 시도하십시오", "bookmark-save": "북마크를 저장할 수 없습니다", - "cache-file-find": "캐시된 이미지를 찾을 수 없습니다. 다시 로드하고 시도하세요.", + "cache-file-find": "캐시된 이미지를 찾을 수 없습니다. 새로고침하고 다시 시도하세요.", "name-required": "이름은 비워둘 수 없습니다", - "reading-list-permission": "이 독서 목록에 대한 권한이 없거나 목록이 존재하지 않습니다", - "generic-read-progress": "진행 상황 저장 중 문제가 발생했습니다", + "reading-list-permission": "이 읽기 목록에 대한 권한이 없거나 목록이 존재하지 않습니다", + "generic-read-progress": "진행 상황을 저장하는 중에 문제가 발생했습니다", "valid-number": "유효한 페이지 번호여야 합니다", - "reading-list-updated": "업데이트되었습니다", + "reading-list-updated": "업데이트됨", "reading-list-item-delete": "항목을 삭제할 수 없습니다", - "series-restricted": "사용자는 이 시리즈에 접근할 수 없습니다", - "generic-reading-list-update": "독서 목록 업데이트 중 문제가 발생했습니다", - "generic-reading-list-create": "독서 목록 생성 중 문제가 발생했습니다", - "generic-scrobble-hold": "보류 추가 중 오류가 발생했습니다", - "libraries-restricted": "사용자는 어떤 라이브러리에도 접근할 수 없습니다", + "series-restricted": "사용자는 이 시리즈에 액세스할 수 없습니다", + "generic-reading-list-update": "읽기 목록을 업데이트하는 중에 문제가 발생했습니다", + "generic-reading-list-create": "읽기 목록을 생성하는 중에 문제가 발생했습니다", + "generic-scrobble-hold": "보류를 추가하는 중에 오류가 발생했습니다", + "libraries-restricted": "사용자는 라이브러리에 액세스할 수 없습니다", "no-series-collection": "컬렉션에 대한 시리즈를 가져올 수 없습니다", - "generic-series-delete": "시리즈 삭제 중 문제가 발생했습니다", - "series-updated": "성공적으로 업데이트되었습니다", + "generic-series-delete": "시리즈를 삭제하는 중에 문제가 발생했습니다", + "series-updated": "성공적으로 업데이트됨", "update-metadata-fail": "메타데이터를 업데이트할 수 없습니다", - "encode-as-warning": "PNG로 변환할 수 없습니다. 커버의 경우 '커버 새로 고침'을 사용하세요. 북마크와 파비콘은 다시 인코딩할 수 없습니다.", - "ip-address-invalid": "IP 주소 '{0}'가 유효하지 않습니다", + "encode-as-warning": "PNG로 변환할 수 없습니다. 표지의 경우 표지 새로 고침을 사용하십시오. 북마크와 파비콘은 다시 인코딩할 수 없습니다.", + "ip-address-invalid": "IP 주소 '{0}'이(가) 잘못되었습니다", "bookmark-dir-permissions": "북마크 디렉토리에 Kavita가 사용할 수 있는 올바른 권한이 없습니다", - "generic-series-update": "시리즈 업데이트 중 오류가 발생했습니다", - "total-backups": "총 백업 수는 1에서 30 사이여야 합니다", + "generic-series-update": "시리즈를 업데이트하는 중에 오류가 발생했습니다", + "total-backups": "총 백업은 1에서 30 사이여야 합니다", "stats-permission-denied": "다른 사용자의 통계를 볼 권한이 없습니다", - "total-logs": "총 로그 수는 1에서 30 사이여야 합니다", - "url-not-valid": "URL이 유효한 이미지를 반환하지 않거나 인증이 필요합니다", - "generic-cover-series-save": "시리즈에 커버 이미지를 저장할 수 없습니다", - "generic-cover-collection-save": "컬렉션에 커버 이미지를 저장할 수 없습니다", - "generic-user-pref": "환경 설정 저장 중 문제가 발생했습니다", - "generic-cover-reading-list-save": "독서 목록에 커버 이미지를 저장할 수 없습니다", - "generic-cover-chapter-save": "챕터에 커버 이미지를 저장할 수 없습니다", - "generic-cover-library-save": "라이브러리에 커버 이미지를 저장할 수 없습니다", - "opds-disabled": "이 서버에서 OPDS가 활성화되어 있지 않습니다", - "access-denied": "접근 권한이 없습니다", - "reset-chapter-lock": "챕터의 커버 잠금 재설정에 실패했습니다", - "on-deck": "다음에 읽을 것", - "browse-on-deck": "다음에 읽을 것 탐색", - "reading-lists": "독서 목록", + "total-logs": "총 로그는 1에서 30 사이여야 합니다", + "url-not-valid": "URL이 유효한 이미지를 반환하지 않거나 승인이 필요합니다", + "generic-cover-series-save": "표지 이미지를 시리즈에 저장할 수 없습니다", + "generic-cover-collection-save": "컬렉션에 표지 이미지를 저장할 수 없습니다", + "generic-user-pref": "환경설정을 저장하는 중에 문제가 발생했습니다", + "generic-cover-reading-list-save": "읽기 목록에 표지 이미지를 저장할 수 없습니다", + "generic-cover-chapter-save": "표지 이미지를 챕터에 저장할 수 없습니다", + "generic-cover-library-save": "표지 이미지를 라이브러리에 저장할 수 없습니다", + "opds-disabled": "이 서버에서 OPDS를 사용할 수 없습니다", + "access-denied": "액세스 권한이 없습니다", + "reset-chapter-lock": "챕터에 대한 표지 잠금을 재설정할 수 없습니다", + "on-deck": "계속 읽기", + "browse-on-deck": "계속 읽기에서 찾아보기", + "reading-lists": "읽기 목록", "libraries": "모든 라이브러리", "generic-user-delete": "사용자를 삭제할 수 없습니다", - "recently-added": "최근 추가됨", + "recently-added": "최근에 추가됨", "collections": "모든 컬렉션", - "browse-collections": "컬렉션으로 탐색", - "reading-list-restricted": "독서 목록이 존재하지 않거나 접근 권한이 없습니다", + "browse-collections": "컬렉션에서 찾아보기", + "reading-list-restricted": "읽기 목록이 없거나 액세스 권한이 없습니다", "query-required": "쿼리 매개변수를 전달해야 합니다", - "search-description": "시리즈, 컬렉션 또는 독서 목록 검색", + "search-description": "시리즈, 컬렉션 또는 읽기 목록 검색", "favicon-doesnt-exist": "파비콘이 존재하지 않습니다", "not-authenticated": "사용자가 인증되지 않았습니다", "anilist-cred-expired": "AniList 자격 증명이 만료되었거나 설정되지 않았습니다", - "scrobble-bad-payload": "스크로블 제공자로부터 잘못된 페이로드를 받았습니다", - "bad-copy-files-for-download": "다운로드를 위한 임시 디렉토리에 파일을 복사할 수 없습니다.", + "scrobble-bad-payload": "스크로블 공급자의 잘못된 페이로드", + "bad-copy-files-for-download": "임시 디렉토리 아카이브 다운로드에 파일을 복사할 수 없습니다.", "search": "검색", - "theme-doesnt-exist": "테마 파일이 없거나 유효하지 않습니다", - "generic-create-temp-archive": "임시 아카이브 생성 중 문제가 발생했습니다", + "theme-doesnt-exist": "테마 파일이 없거나 유효하지 않음", + "generic-create-temp-archive": "임시 보관 파일을 만드는 중에 문제가 발생했습니다", "epub-html-missing": "해당 페이지에 적합한 HTML을 찾을 수 없습니다", - "epub-malformed": "파일이 손상되었습니다! 읽을 수 없습니다.", + "epub-malformed": "파일 형식이 잘못되었습니다! 읽을 수 없습니다.", "collection-tag-title-required": "컬렉션 제목은 비워둘 수 없습니다", - "collection-tag-duplicate": "이 이름의 컬렉션이 이미 존재합니다", - "device-not-created": "이 장치는 아직 존재하지 않습니다. 먼저 생성하세요", - "device-duplicate": "이 이름의 장치가 이미 존재합니다", - "send-to-permission": "EPUB 또는 PDF가 아닌 파일은 Kindle에서 지원되지 않으므로 장치로 보낼 수 없습니다", - "user-no-access-library-from-series": "사용자는 이 시리즈가 속한 라이브러리에 접근할 수 없습니다", - "reading-list-name-exists": "이 이름의 독서 목록이 이미 존재합니다", + "collection-tag-duplicate": "이 이름을 가진 컬렉션이 이미 존재합니다", + "device-not-created": "이 장치는 아직 존재하지 않습니다. 먼저 생성해주세요", + "device-duplicate": "이 이름을 가진 장치가 이미 존재합니다", + "send-to-permission": "Kindle에서 지원되지 않는 비 EPUB 또는 PDF를 장치로 보낼 수 없음", + "user-no-access-library-from-series": "사용자는 이 시리즈가 속한 라이브러리에 액세스할 수 없습니다", + "reading-list-name-exists": "이 이름의 읽기 목록이 이미 있습니다", "series-restricted-age-restriction": "사용자는 연령 제한으로 인해 이 시리즈를 볼 수 없습니다", "book-num": "책 {0}", "issue-num": "이슈 {0}{1}", - "browse-recently-added": "최근 추가된 항목 탐색", - "browse-reading-lists": "독서 목록으로 탐색", - "browse-libraries": "라이브러리로 탐색", - "unable-to-register-k+": "오류로 인해 라이센스를 등록할 수 없습니다. Kavita+ 지원팀에 연락하세요", - "want-to-read": "읽고 싶은 책", - "browse-want-to-read": "읽고 싶은 책 탐색", + "browse-recently-added": "최근 추가된 항목에서 찾아보기", + "browse-reading-lists": "읽기 목록에서 찾아보기", + "browse-libraries": "라이브러리에서 찾아보기", + "unable-to-register-k+": "오류로 인해 라이선스를 등록할 수 없습니다. Kavita+ 지원 문의", + "want-to-read": "읽고 싶어요", + "browse-want-to-read": "읽고 싶어요에서 찾아보기", "collection-deleted": "컬렉션이 삭제되었습니다", "smart-filters": "스마트 필터", - "browse-smart-filters": "스마트 필터로 탐색", + "browse-smart-filters": "스마트 필터로 찾아보기", "smart-filter-doesnt-exist": "스마트 필터가 존재하지 않습니다", - "browse-external-sources": "외부 소스 탐색", - "external-source-already-in-use": "이 외부 소스로 이미 스트림이 존재합니다", + "browse-external-sources": "외부 소스 찾아보기", + "external-source-already-in-use": "이 외부 소스가 포함된 기존 스트림이 있습니다", "dashboard-stream-doesnt-exist": "대시보드 스트림이 존재하지 않습니다", "external-source-already-exists": "외부 소스가 이미 존재합니다", - "sidenav-stream-doesnt-exist": "사이드바 스트림이 존재하지 않습니다", + "sidenav-stream-doesnt-exist": "사이드 내비게이션 스트림이 존재하지 않습니다", "external-source-doesnt-exist": "외부 소스가 존재하지 않습니다", "external-sources": "외부 소스", - "external-source-required": "ApiKey와 호스트가 필요합니다", - "smart-filter-already-in-use": "이 스마트 필터로 이미 스트림이 존재합니다", - "invalid-email": "사용자의 이메일이 유효하지 않습니다. 링크는 로그를 확인하세요.", - "browse-more-in-genre": "{0}의 더 많은 항목 탐색", - "more-in-genre": "{0} 장르의 더 많은 항목", - "recently-updated": "최근 업데이트됨", - "browse-recently-updated": "최근 업데이트된 항목 탐색", - "email-not-enabled": "이 서버에서는 이메일이 활성화되지 않았습니다. 이 작업을 수행할 수 없습니다.", - "send-to-size-limit": "보내려는 파일이 이메일 제공업체에 비해 너무 큽니다", - "send-to-unallowed": "본인 소유가 아닌 장치로 보낼 수 없습니다", - "unable-to-reset-k+": "오류로 인해 Kavita+ 라이센스를 재설정할 수 없습니다. Kavita+ 지원팀에 연락하세요", - "check-updates": "업데이트 확인", - "license-check": "라이센스 확인", - "process-scrobbling-events": "스크로블링 이벤트 처리", - "report-stats": "통계 보고", - "check-scrobbling-tokens": "스크로블링 토큰 확인", - "cleanup": "정리", - "remove-from-want-to-read": "읽고 싶은 목록에서 제거", - "scan-libraries": "라이브러리 스캔", - "kavita+-data-refresh": "Kavita+ 데이터 새로 고침", - "backup": "백업", - "update-yearly-stats": "연간 통계 업데이트", - "process-processed-scrobbling-events": "처리된 스크로블링 이벤트 처리", - "account-email-invalid": "관리자 계정의 이메일이 유효하지 않습니다. 테스트 이메일을 보낼 수 없습니다.", - "email-settings-invalid": "이메일 설정에 누락된 정보가 있습니다. 모든 이메일 설정이 저장되었는지 확인하세요.", - "collection-already-exists": "컬렉션이 이미 존재합니다", - "error-import-stack": "MAL 스택 가져오기 중 문제가 발생했습니다", - "generic-cover-person-save": "인물에 커버 이미지를 저장할 수 없습니다", - "generic-cover-volume-save": "볼륨에 커버 이미지를 저장할 수 없습니다", - "person-doesnt-exist": "사람이 존재하지 않습니다", - "person-name-required": "개인 이름은 필수 항목이며 null일 수 없습니다", - "person-name-unique": "개인 이름은 고유해야 합니다", - "person-image-doesnt-exist": "CoversDB에 사람이 존재하지 않습니다", - "kavitaplus-restricted": "Kavita+만 해당", - "email-taken": "이미 사용중인 이메일", - "dashboard-stream-only-delete-smart-filter": "대시보드에서 스마트 필터 스트림만 삭제할 수 있습니다", - "sidenav-stream-only-delete-smart-filter": "사이드 메뉴에서 스마트 필터 스트림만 삭제할 수 있습니다", - "smart-filter-name-required": "스마트 필터 이름이 필요합니다", - "smart-filter-system-name": "시스템 제공 스트림 이름은 사용할 수 없습니다", - "aliases-have-overlap": "하나 이상의 별명이 다른 사용자와 중복되어 업데이트할 수 없습니다", - "generated-reading-profile-name": "{0}(으)로부터 생성됨" + "external-source-required": "ApiKey 및 호스트가 필요합니다", + "smart-filter-already-in-use": "이 스마트 필터가 포함된 기존 스트림이 있습니다" } diff --git a/API/I18N/ms.json b/API/I18N/ms.json index a7191f037..c4f9371a1 100644 --- a/API/I18N/ms.json +++ b/API/I18N/ms.json @@ -24,6 +24,7 @@ "generic-invite-user": "Terdapat masalah jemputan pengguna. Sila semak log.", "invalid-email-confirmation": "Pengesahan e-mel tidak sah", "generic-user-email-update": "Pengguna e-mel ini tidak dapat di kemas kinikan. Semak log.", + "bad-credentials": "Bukti kelayakan anda tidak betul", "locked-out": "Anda telah di sekat kerana terlalu banyak membuat percubaan kebenaran. Sila tunggu 10 minit.", "disabled-account": "Akaun anda telah di sekat. Hubungi pentadbir server.", "register-user": "Sesuatu telah berlaku kesilapan semasa pendaftaran pengguna", diff --git a/API/I18N/nb_NO.json b/API/I18N/nb_NO.json deleted file mode 100644 index be6db6e47..000000000 --- a/API/I18N/nb_NO.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "bookmark-save": "Kunne ikke lagre bokmerke" -} diff --git a/API/I18N/nl.json b/API/I18N/nl.json index e9597a6cb..29f71cf95 100644 --- a/API/I18N/nl.json +++ b/API/I18N/nl.json @@ -7,6 +7,7 @@ "generic-password-update": "Er is een onverwachte fout opgetreden bij het bevestigen van het nieuwe wachtwoord", "locked-out": "U bent uitgesloten door te veel autorisatiepogingen. Wacht alsjeblieft 10 minuten.", "register-user": "Er is iets misgegaan bij het registreren van de gebruiker", + "bad-credentials": "Uw inloggegevens zijn niet correct", "disabled-account": "Uw account is uitgeschakeld. Neem contact op met de serverbeheerder.", "validate-email": "Er is een probleem opgetreden bij het valideren van uw e-mailadres: {0}", "confirm-token-gen": "Er is een probleem opgetreden bij het genereren van een bevestigingstoken", @@ -155,8 +156,5 @@ "chapter-num": "Hoofdstuk {0}", "generic-scrobble-hold": "Er is een fout opgetreden bij het toevoegen van de bewaarplicht", "on-deck": "Aan het lezen", - "browse-on-deck": "Aan Het Lezen doorbladeren", - "email-not-enabled": "Email is niet aangezet op deze server. Je kan deze actie niet uitvoeren.", - "collection-deleted": "Collectie verwijderd", - "invalid-email": "De email op het bestand van de gebruiker is geen geldige email. Bekijk het lognoek voor mogelijke weblinks." + "browse-on-deck": "Aan Het Lezen doorbladeren" } diff --git a/API/I18N/pl.json b/API/I18N/pl.json index 5372fddc0..f51991dda 100644 --- a/API/I18N/pl.json +++ b/API/I18N/pl.json @@ -1,4 +1,5 @@ { + "bad-credentials": "Twoje dane uwierzytelniające są nieprawidłowe", "disabled-account": "Twoje konto jest wyłączone. Skontaktuj się z administratorem serwera.", "register-user": "Coś poszło nie tak podczas rejestracji użytkownika", "validate-email": "Wystąpił problem z weryfikacją adresu e-mail: {0}", @@ -52,162 +53,8 @@ "collection-doesnt-exist": "Kolekcja nie istnieje", "generic-device-create": "Wystąpił błąd podczas tworzenia urządzenia", "generic-device-update": "Wystąpił błąd podczas aktualizacji urządzenia", - "send-to-kavita-email": "Funkcja Wyślij do urządzenia nie może być używana bez konfiguracji poczty e-mail", + "send-to-kavita-email": "Funkcja Wyślij do urządzenia nie może być używana z usługą e-mail Kavita. Należy skonfigurować własną.", "bookmark-doesnt-exist": "Zakładka nie istnieje", "series-doesnt-exist": "Seria nie istnieje", - "must-be-defined": "{0} musi być zdefiniowane", - "email-not-enabled": "E-mail nie jest włączony na tym serwerze. Nie można wykonać tej akcji.", - "send-to-size-limit": "Plik(i), które próbujesz wysłać, są zbyt duże dla twojego dostawcy poczty e-mail", - "generic-favicon": "Wystąpił problem z pobieraniem favicon dla domeny", - "invalid-filename": "Nieprawidłowa nazwa pliku", - "file-doesnt-exist": "Plik nie istnieje", - "library-name-exists": "Nazwa biblioteki już istnieje. Wybierz unikalną nazwę dla serwera.", - "no-library-access": "Użytkownik nie ma dostępu do tej biblioteki", - "user-doesnt-exist": "Użytkownik nie istnieje", - "library-doesnt-exist": "Biblioteka nie istnieje", - "invalid-path": "Nieprawidłowa ścieżka", - "generic-library-update": "Pojawił się krytyczny problem z aktualizacją biblioteki.", - "bookmark-save": "Nie udało się zapisać zakładki", - "generic-library": "Wystąpił błąd krytyczny. Spróbuj ponownie.", - "no-image-for-page": "Brak takiego obrazu dla strony {0}. Spróbuj odświeżyć, aby zezwolić na ponowne buforowanie.", - "send-to-unallowed": "Nie można wysyłać na urządzenie, które nie należy do Ciebie", - "delete-library-while-scan": "Nie można usunąć biblioteki podczas skanowania. Poczekaj na zakończenie skanowania lub uruchom ponownie Kavitę, a następnie spróbuj usunąć bibliotekę", - "generic-clear-bookmarks": "Nie udało się wyczyścić zakładek", - "bookmark-permission": "Nie masz uprawnień do dodawania/usuwania zakładek", - "cache-file-find": "Nie można znaleźć obrazu z pamięci podręcznej. Przeładuj i spróbuj ponownie.", - "collection-deleted": "Kolekcja usunięta", - "invalid-access": "Nieprawidłowy dostęp", - "pdf-doesnt-exist": "Plik PDF nie istnieje, gdy powinien", - "perform-scan": "Przeprowadź skanowanie tej serii lub biblioteki i spróbuj ponownie", - "generic-read-progress": "Wystąpił problem z zapisywaniem postępu", - "smart-filters": "Inteligentne filtry", - "browse-smart-filters": "Przeglądaj według inteligentnych filtrów", - "smart-filter-doesnt-exist": "Inteligentny filtr nie istnieje", - "send-to-permission": "Nie można wysłać plików innych niż EPUB lub PDF na urządzenia, ponieważ nie są one obsługiwane przez Kindle", - "book-num": "Książka {0}", - "no-series": "Nie udało się uzyskać serii dla biblioteki", - "no-series-collection": "Nie udało się uzyskać serii dla kolekcji", - "generic-relationship": "Wystąpił problem z aktualizacją powiązań", - "ip-address-invalid": "Adres IP \"{0}\" jest nieprawidłowy", - "generic-cover-series-save": "Nie można zapisać obrazu okładki do serii", - "generic-cover-collection-save": "Nie można zapisać obrazu okładki dla kolekcji", - "generic-cover-chapter-save": "Nie można zapisać obrazu okładki dla rozdziału", - "generic-user-pref": "Wystąpił problem z zapisaniem preferencji", - "opds-disabled": "OPDS nie jest włączony na tym serwerze", - "want-to-read": "Chcę przeczytać", - "browse-libraries": "Przeglądanie według bibliotek", - "collections": "Wszystkie Kolekcje", - "browse-collections": "Przeglądaj według Kolekcji", - "external-source-required": "Klucz API i Host jest wymagany", - "bad-copy-files-for-download": "Nie można skopiować plików do pobieranego archiwum katalogu tymczasowego.", - "generic-create-temp-archive": "Wystąpił problem z tworzeniem archiwum tymczasowego", - "epub-html-missing": "Nie można znaleźć odpowiedniego html dla tej strony", - "check-updates": "Sprawdź aktualizacje", - "license-check": "Sprawdź licencje", - "report-stats": "Statystyki raportów", - "cleanup": "Wyczyść", - "kavita+-data-refresh": "Odśwież dane Kavita+", - "backup": "Kopia zapasowa", - "more-in-genre": "Więcej w gatunku {0}", - "browse-more-in-genre": "Przeglądaj więcej w {0}", - "external-sources": "Zewnętrzne źródła", - "browse-external-sources": "Przeglądaj zewnętrzne źródła", - "recently-updated": "Niedawno zaktualizowane", - "browse-recently-updated": "Przeglądaj niedawno zaktualizowane", - "external-source-already-exists": "Zewnętrzne źródło już istnieje", - "collection-tag-duplicate": "Kolekcja o tej nazwie już istnieje", - "device-not-created": "To urządzenie jeszcze nie istnieje. Utwórz je najpierw", - "series-restricted-age-restriction": "Użytkownik nie może wyświetlić tej serii ze względu na ograniczenia wiekowe", - "series-updated": "Pomyślnie zaktualizowano", - "update-metadata-fail": "Nie udało się zaktualizować metadanych", - "total-logs": "Łączna liczba logów musi mieścić się w przedziale od 1 do 30", - "stats-permission-denied": "Nie masz uprawnień do wyświetlania statystyk innego użytkownika", - "url-not-valid": "Adres URL nie zwraca prawidłowego obrazu lub wymaga autoryzacji", - "generic-user-delete": "Nie można usunąć użytkownika", - "access-denied": "Nie masz dostępu", - "browse-recently-added": "Przeglądaj Ostatnio dodane", - "recently-added": "Ostatnio dodane", - "search": "Szukaj", - "volume-num": "Tom {0}", - "device-duplicate": "Urządzenie o tej nazwie już istnieje", - "unable-to-reset-k+": "Nie można zresetować licencji Kavita+ z powodu błędu. Skontaktuj się z pomocą techniczną Kavita+", - "issue-num": "Wydanie {0}{1}", - "chapter-num": "Rozdział {0}", - "libraries-restricted": "Użytkownik nie ma dostępu do żadnych bibliotek", - "generic-series-delete": "Wystąpił problem z usunięciem serii", - "generic-series-update": "Wystąpił błąd podczas aktualizacji serii", - "age-restriction-not-applicable": "Bez ograniczeń", - "bookmark-dir-permissions": "Folder zakładek nie ma prawidłowych uprawnień do użycia przez Kavitę", - "epub-malformed": "Plik jest zniekształcony! Nie można odczytać.", - "total-backups": "Łączna liczba kopii zapasowych musi mieścić się w przedziale od 1 do 30", - "user-no-access-library-from-series": "Użytkownik nie ma dostępu do biblioteki, do której należy ta seria", - "generic-cover-library-save": "Nie można zapisać obrazu okładki dla biblioteki", - "browse-want-to-read": "Przeglądaj Chcę przeczytać", - "scan-libraries": "Skanuj Biblioteki", - "external-source-doesnt-exist": "Zewnętrzne źródło nie istnieje", - "update-yearly-stats": "Aktualizuj roczne statystyki", - "not-authenticated": "Użytkownik nie jest uwierzytelniony", - "unable-to-register-k+": "Nie można zarejestrować licencji z powodu błędu. Skontaktuj się z pomocą techniczną Kavita+", - "anilist-cred-expired": "Poświadczenia AniList wygasły lub nie zostały ustawione", - "collection-tag-title-required": "Tytuł Kolekcji nie może być pusty", - "job-already-running": "Zadanie już uruchomione", - "encode-as-warning": "Nie można konwertować do formatu PNG. W przypadku okładek należy użyć opcji Odśwież okładki. Zakładek i ikon ulubionych nie można zakodować z powrotem.", - "favicon-doesnt-exist": "Favicon nie istnieje", - "theme-doesnt-exist": "Brak pliku motywu lub jest on nieprawidłowy", - "libraries": "Wszystkie biblioteki", - "email-settings-invalid": "Brak informacji o ustawieniach poczty e-mail. Upewnij się, że wszystkie ustawienia poczty e-mail zostały zapisane.", - "series-restricted": "Użytkownik nie ma dostępu do tej serii", - "generic-cover-reading-list-save": "Nie można zapisać obrazu okładki na liście czytelniczej", - "browse-on-deck": "Przeglądaj Czytaj dalej", - "reading-list-name-exists": "Lista czytelnicza o tej nazwie już istnieje", - "name-required": "Nazwa nie może być pusta", - "valid-number": "Wprowadź poprawny numer strony", - "duplicate-bookmark": "Duplikat zakładki już istnieje", - "reading-list-position": "Nie można zaktualizować położenia", - "reading-list-deleted": "Lista czytelnicza została usunięta", - "generic-reading-list-delete": "Wystąpił problem z usunięciem listy czytelniczej", - "url-required": "Musisz podać adres URL, aby użyć", - "on-deck": "Czytaj dalej", - "reading-list-doesnt-exist": "Lista czytelnicza nie istnieje", - "reading-list-updated": "Zaktualizowano", - "generic-reading-list-create": "Wystąpił problem z utworzeniem listy czytelniczej", - "generic-scrobble-hold": "Wystąpił błąd podczas oznaczania jako wstrzymane", - "reset-chapter-lock": "Nie można zresetować blokady okładki dla rozdziału", - "reading-lists": "Listy czytelnicze", - "browse-reading-lists": "Przeglądaj według list czytelniczych", - "reading-list-restricted": "Lista czytelnicza nie istnieje lub nie masz do niej dostępu", - "search-description": "Szukaj serii, kolekcji lub list czytelniczych", - "reading-list-title-required": "Tytuł listy czytelniczej nie może być pusty", - "progress-must-exist": "Postęp musi istnieć u użytkownika", - "reading-list-permission": "Nie masz uprawnień do tej listy czytelniczej lub lista nie istnieje", - "reading-list-item-delete": "Nie można usunąć elementu(ów)", - "generic-reading-list-update": "Wystąpił problem z aktualizacją listy czytelniczej", - "collection-already-exists": "Kolekcja już istnieje", - "generic-cover-person-save": "Nie udało się zapisać obrazu dla Osoby", - "generic-cover-volume-save": "Nie udało się zapisać obrazu okładki dla Tomu", - "external-source-already-in-use": "Istnieje już strumień z tym zewnętrznym źródłem", - "scrobble-bad-payload": "Nieprawidłowy payloadod dostawcy Scrobblowania", - "process-scrobbling-events": "Zdarzenia związane z procesem Scrobblowania", - "process-processed-scrobbling-events": "Przetworzone zdarzenia Scrobblowania", - "error-import-stack": "Wystąpił problem z importowaniem MAL Stack", - "account-email-invalid": "Adres e-mail dla konta administratora nie jest prawidłowy. Nie można wysłać testowej wiadomości e-mail.", - "query-required": "Należy przekazać parametr zapytania", - "smart-filter-already-in-use": "Istnieje strumień z tym inteligentnym filtrem", - "dashboard-stream-doesnt-exist": "Strumień pulpitu nawigacyjnego nie istnieje", - "sidenav-stream-doesnt-exist": "Strumień SideNav nie istnieje", - "remove-from-want-to-read": "Wyczyść Chcesz przeczytać", - "check-scrobbling-tokens": "Sprawdź tokeny Scrobblowania", - "invalid-email": "Adres e-mail użytkownika w pliku nie jest prawidłowy. Zobacz logi dla wszystkich linków.", - "person-doesnt-exist": "Osoba nie istnieje", - "person-name-required": "Nazwa osoby jest wymagana i nie może mieć wartości null", - "person-name-unique": "Nazwa osoby musi być unikatowa", - "person-image-doesnt-exist": "Osoba nie istnieje w CoversDB", - "email-taken": "Adres e-mail jest już używany", - "kavitaplus-restricted": "Jest to dostępne tylko dla Kavita+", - "smart-filter-name-required": "Inteligentny filtr wymaga nazwy", - "sidenav-stream-only-delete-smart-filter": "Tylko inteligentne filtry mogą zostać usunięte z panelu bocznego", - "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów może zostać usunięte z głównego panelu", - "smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem", - "aliases-have-overlap": "Jeden lub więcej aliasów pokrywa się z innymi osobami, nie można ich zaktualizować", - "generated-reading-profile-name": "Wygenerowano z {0}" + "must-be-defined": "{0} musi być zdefiniowane" } diff --git a/API/I18N/pt.json b/API/I18N/pt.json index 726c843bb..bbd8baee1 100644 --- a/API/I18N/pt.json +++ b/API/I18N/pt.json @@ -1,4 +1,5 @@ { + "bad-credentials": "As credenciais não estão corretas", "password-required": "Se não for administrador, tem de introduzir a sua palavra passe para alterar a sua conta", "confirm-token-gen": "Ocorreu um problema a gerar um token de confirmação", "age-restriction-update": "Ocorreu um erro ao atualizar a restrição de idade", @@ -19,7 +20,7 @@ "generic-device-create": "Ocorreu um erro ao criar o dispositivo", "generic-device-update": "Ocorreu um erro ao atualizar o dispositivo", "greater-0": "{0} tem de ser superior a 0", - "send-to-kavita-email": "Enviar para dispositivo não pode ser usado sem a configuração de Email", + "send-to-kavita-email": "Enviar para dispositivo não pode ser usado com o serviço de email do Kavita. Por favor configure o seu serviço de email.", "bookmark-doesnt-exist": "Marcador inexistente", "file-doesnt-exist": "Ficheiro inexistente", "delete-library-while-scan": "Não pode eliminar a biblioteca enquanto uma análise está em curso. Por favor aguarde que a análise termine ou reinicie o Kavita e depois tente eliminar novamente", @@ -168,46 +169,5 @@ "external-sources": "Fontes Externas", "external-source-required": "ApiKey e Host requeridos", "smart-filter-already-in-use": "Existe um stream com este Filtro Inteligente", - "collection-deleted": "Coleção eliminada", - "dashboard-stream-doesnt-exist": "Stream do Painel Principal não existe", - "invalid-email": "O email guardado para o utilizador não é válido. Verifique se existem links nos logs.", - "sidenav-stream-doesnt-exist": "Stream do SideNav não existe", - "browse-more-in-genre": "Ver mais em {0}", - "more-in-genre": "Mais do Género {0}", - "recently-updated": "Recém-Atualizados", - "browse-recently-updated": "Ver Recém-Atualizados", - "unable-to-reset-k+": "Não foi possível redefinir a licença do Kavita+ devido a um erro. Entre em contacto com o suporte Kavita +", - "email-not-enabled": "O email não está habilitado neste servidor. Não pode executar esta ação.", - "send-to-unallowed": "Não pode enviar para um dispositivo que não é seu", - "send-to-size-limit": "Os ficheiros que está a tentar enviar são demasiado grandes para o seu serviço de email", - "backup": "Backup", - "check-scrobbling-tokens": "Verificar Tokens de Scrobbling", - "cleanup": "Limpar", - "process-processed-scrobbling-events": "Processar Eventos de Scrobbling Processados", - "remove-from-want-to-read": "Limpeza de Leituras Desejadas", - "account-email-invalid": "O e-mail registado para a conta admin não é um e-mail válido. Não pode enviar e-mail de teste.", - "email-settings-invalid": "As definições de e-mail têm informação em falta. Certifique-se de que todas as definições de e-mail estão gravadas.", - "report-stats": "Relatório de Estatísticas", - "check-updates": "Verificar Atualizações", - "license-check": "Verificação de Licença", - "process-scrobbling-events": "Processar Eventos de Scrobbling", - "scan-libraries": "Analisar Bibliotecas", - "kavita+-data-refresh": "Atualização de dados do Kavita+", - "update-yearly-stats": "Atualizar estatísticas anuais", - "collection-already-exists": "Coleção já existente", - "error-import-stack": "Ocorreu um problema a importar uma pilha do MAL", - "generic-cover-person-save": "Não foi possível gravar a imagem de capa na Pessoa", - "generic-cover-volume-save": "Não foi possível gravar a imagem de capa no Volume", - "person-doesnt-exist": "Pessoa não existe", - "person-name-required": "O nome da pessoa é obrigatório e não pode nulo", - "person-name-unique": "O nome da pessoa tem de ser único", - "person-image-doesnt-exist": "A pessoa não existe na CoversDB", - "email-taken": "Email já em uso", - "kavitaplus-restricted": "Ação restrita ao Kavita+", - "sidenav-stream-only-delete-smart-filter": "Apenas os filtros inteligentes podem ser removidos da Navegação Lateral", - "dashboard-stream-only-delete-smart-filter": "Apenas os filtros inteligentes podem ser removidos do painel", - "smart-filter-system-name": "Não pode usar o nome de um fluxo disponibilizado pelo sistema", - "smart-filter-name-required": "Nome requerido para o filtro inteligente", - "aliases-have-overlap": "Um ou mais pseudónimos sobrepõem-se com outras pessoas, não vai ser possível atualizar", - "generated-reading-profile-name": "Gerado de {0}" + "collection-deleted": "Coleção eliminada" } diff --git a/API/I18N/pt_BR.json b/API/I18N/pt_BR.json index 418e0ea3b..c29522ff0 100644 --- a/API/I18N/pt_BR.json +++ b/API/I18N/pt_BR.json @@ -1,7 +1,7 @@ { "generic-error": "Alguma coisa deu errado. Por favor, tente outra vez", "collection-doesnt-exist": "A coleção não existe", - "send-to-kavita-email": "Enviar para dispositivo não pode ser usado sem configuração de e-mail", + "send-to-kavita-email": "Enviar para o dispositivo não pode ser usado com o serviço de e-mail da Kavita. Por favor, configure o seu próprio.", "volume-doesnt-exist": "O volume não existe", "no-cover-image": "Sem imagem de capa", "invalid-filename": "Nome de arquivo inválido", @@ -11,6 +11,7 @@ "delete-library-while-scan": "Você não pode excluir uma biblioteca enquanto uma verificação estiver em andamento. Aguarde a conclusão da verificação ou reinicie o Kavita e tente excluir", "generic-library-update": "Ocorreu um problema crítico ao atualizar a biblioteca.", "confirm-email": "Você deve confirmar seu e-mail primeiro", + "bad-credentials": "Suas credenciais não estão corretas", "locked-out": "Você foi bloqueado por muitas tentativas de autorização. Aguarde 10 minutos.", "validate-email": "Ocorreu um problema ao validar seu e-mail: {0}", "denied": "Não permitido", @@ -170,44 +171,5 @@ "external-source-required": "ApiKey e Host necessários", "smart-filter-already-in-use": "Existe um stream com este Filtro Inteligente", "browse-external-sources": "Navegar em Fontes Externas", - "external-sources": "Fontes Externas", - "invalid-email": "O e-mail registrado para o usuário não é um e-mail válido. Veja os registros para quaisquer links.", - "browse-more-in-genre": "Navegue por mais em {0}", - "more-in-genre": "Mais em Gênero {0}", - "recently-updated": "Atualizado Recentemente", - "browse-recently-updated": "Navegar Atualizado Recentemente", - "unable-to-reset-k+": "Não foi possível redefinir a licença Kavita+ devido a um erro. Entre em contato com o suporte Kavita +", - "send-to-unallowed": "Você não pode enviar para um dispositivo que não seja seu", - "email-not-enabled": "O e-mail não está ativado neste servidor. Você não pode executar esta ação.", - "send-to-size-limit": "Os arquivos que você está tentando enviar são muito grandes para o seu provedor de e-mail", - "check-updates": "Verificar por Atualizações", - "license-check": "Verificar Licença", - "process-scrobbling-events": "Eventos de Scrobbling de Processo", - "report-stats": "Estatísticas do Relatório", - "process-processed-scrobbling-events": "Processar eventos de Scrobbling processados", - "remove-from-want-to-read": "Limpar Quero Ler", - "scan-libraries": "Escanear Bibliotecas", - "backup": "Backup", - "update-yearly-stats": "Atualizar estatísticas anuais", - "check-scrobbling-tokens": "Verificar os Tokens de Scrobbling", - "cleanup": "Limpar", - "kavita+-data-refresh": "Atualização de dados Kavita+", - "account-email-invalid": "O e-mail registrado para a conta de administrador não é um e-mail válido. Não é possível enviar e-mail de teste.", - "email-settings-invalid": "Faltam informações nas configurações de e-mail. Certifique-se de que todas as configurações de e-mail estejam salvas.", - "error-import-stack": "Ocorreu um problema ao importar a pilha MAL", - "collection-already-exists": "A coleção já existe", - "generic-cover-person-save": "Não foi possível salvar a imagem da capa em Pessoa", - "generic-cover-volume-save": "Não foi possível salvar a imagem da capa no Volume", - "person-doesnt-exist": "Pessoa não existe", - "person-image-doesnt-exist": "A pessoa não existe no CoversDB", - "person-name-required": "O nome da pessoa é obrigatório e não deve ser nulo", - "person-name-unique": "O nome da pessoa deve ser exclusivo", - "email-taken": "E-mail já em uso", - "kavitaplus-restricted": "Isso é restrito apenas ao Kavita+", - "smart-filter-name-required": "Nome do Filtro Inteligente obrigatório", - "dashboard-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do painel", - "smart-filter-system-name": "Você não pode usar o nome de um fluxo fornecido pelo sistema", - "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral", - "aliases-have-overlap": "Um ou mais dos pseudônimos se sobrepõem a outras pessoas, não pode atualizar", - "generated-reading-profile-name": "Gerado a partir de {0}" + "external-sources": "Fontes Externas" } diff --git a/API/I18N/ru.json b/API/I18N/ru.json index fdea5920f..7757c81c9 100644 --- a/API/I18N/ru.json +++ b/API/I18N/ru.json @@ -1,206 +1,4 @@ { - "confirm-email": "Сначала Вы обязаны подтвердить свою электронную почту", - "generate-token": "Возникла проблема с генерацией токена подтверждения электронной почты. Смотрите журналы", - "invalid-password": "Неверный пароль", - "invalid-email-confirmation": "Неверное подтверждение электронной почты", - "validate-email": "Возникла проблема с проверкой вашей электронной почты: {0}", - "age-restriction-update": "Произошла ошибка при обновлении возрастного ограничения", - "not-accessible": "Ваш сервер недоступен извне", - "email-sent": "Электронная почта отправлена", - "generic-password-update": "При подтверждении нового пароля возникла непредвиденная ошибка", - "user-already-confirmed": "Пользователь уже подтвержден", - "user-migration-needed": "Этот пользователь нуждается в миграции. Пусть они выйдут из системы и войдут в нее, чтобы запустить поток миграции", - "generic-user-update": "При обновлении пользователя возникало исключение", - "disabled-account": "Ваша учетная запись отключена. Обратитесь к администратору сервера.", - "locked-out": "Вы были заблокированы из-за слишком большого количества попыток входа. Пожалуйста, повторите через 10 минут.", - "invalid-token": "Неверный токен", - "generic-user-email-update": "Невозможно обновить электронную почту пользователя. Проверьте журналы.", - "password-updated": "Обновление пароля", - "password-required": "Для изменения учетной записи необходимо ввести существующий пароль, если вы не являетесь администратором", - "share-multiple-emails": "Вы не можете совместно использовать электронную почту в нескольких учетных записях", - "invalid-payload": "Недопустимая нагрузка", - "invalid-email": "Электронная почта пользователя не является действительной. Посмотрите журналы, чтобы найти ссылки.", - "user-already-invited": "Пользователь уже приглашен по этой электронной почте и еще не принял приглашение.", - "unable-to-reset-key": "Что-то пошло не так, не удается сбросить ключ", - "confirm-token-gen": "Возникла проблема с генерацией токена подтверждения", - "denied": "Не разрешено", - "not-accessible-password": "Ваш сервер недоступен. Ссылка для сброса пароля находится в журналах", - "user-already-registered": "Пользователь уже зарегистрирован как {0}", - "register-user": "Что-то пошло не так при регистрации пользователя", - "generic-invite-email": "Возникла проблема с повторной отправкой приглашения по электронной почте", - "nothing-to-do": "Нечего делать", - "username-taken": "Имя пользователя уже занято", - "manual-setup-fail": "Ручная настройка не может быть завершена. Пожалуйста, отмените и создайте приглашение заново", - "forgot-password-generic": "Электронное письмо будет отправлено на адрес электронной почты, если он существует в нашей базе данных", - "no-user": "Пользователь не существует", - "generic-invite-user": "Возникла проблема с приглашением пользователя. Пожалуйста, проверьте журналы.", - "permission-denied": "Вам запрещено выполнять эту операцию", - "invalid-access": "В доступе отказано", - "reading-list-name-exists": "Такой список для чтения уже существует", - "perform-scan": "Пожалуйста, выполните сканирование этой серии или библиотеки и повторите попытку", - "generic-device-create": "При создании устройства возникла ошибка", - "generic-read-progress": "Возникла проблема с сохранением прогресса", - "file-doesnt-exist": "Файл не существует", - "admin-already-exists": "Администратор уже существует", - "send-to-kavita-email": "Отправка на устройство не может быть использована без настройки электронной почты", - "no-image-for-page": "Нет такого изображения для страницы {0}. Попробуйте обновить, для повторного кеширования.", - "reading-list-permission": "У вас нет прав на этот список чтения или список не существует", - "volume-doesnt-exist": "Том не существует", - "generic-library": "Возникла критическая проблема. Пожалуйста, попробуйте еще раз.", - "bookmark-save": "Не удалось сохранить закладку", - "generic-scrobble-hold": "Произошла ошибка при добавлении удержания", - "generic-reading-list-delete": "Возникла проблема с удалением списка для чтения", - "library-doesnt-exist": "Библиотека не существует", - "generic-send-to": "Возникла ошибка при отправке файла(ов) на устройство", - "bookmark-doesnt-exist": "Закладка не существует", - "reading-list-deleted": "Список для чтения был удален", - "generic-reading-list-create": "Возникла проблема с созданием списка для чтения", - "no-cover-image": "Изображение на обложке отсутствует", - "collection-updated": "Коллекция успешно обновлена", - "critical-email-migration": "Возникла проблема при смене электронной почты. Обратитесь в службу поддержки", - "cache-file-find": "Не удалось найти изображение в кэше. Перезагрузитесь и попробуйте снова.", - "duplicate-bookmark": "Дублирующая закладка уже существует", - "collection-tag-duplicate": "Такая коллекция уже существует", - "delete-library-while-scan": "Вы не можете удалить библиотеку во время сканирования. Пожалуйста, дождитесь завершения сканирования или перезапустите Kavita, а затем попробуйте удалить", - "reading-list-updated": "Обновленный", - "collection-doesnt-exist": "Коллекция не существует", - "chapter-doesnt-exist": "Глава не существует", - "generic-library-update": "Возникла критическая проблема с обновлением библиотеки.", - "must-be-defined": "{0} должно быть определено", - "series-restricted": "Пользователь не имеет доступа к этой серии", - "generic-clear-bookmarks": "Не удалось очистить закладки", - "pdf-doesnt-exist": "PDF не существует, когда он должен существовать", - "generic-device-delete": "При удалении устройства возникла ошибка", - "bookmarks-empty": "Закладки не могут быть пустыми", - "valid-number": "Номер страницы должен быть действительным", - "series-doesnt-exist": "Серия не существует", - "no-library-access": "Пользователь не имеет доступа к этой библиотеке", - "reading-list-item-delete": "Не удалось удалить элемент(ы)", - "generic-favicon": "Возникла проблема с получением favicon для домена", - "invalid-filename": "Недопустимое имя файла", - "library-name-exists": "Имя библиотеки уже существует. Пожалуйста, выберите уникальное имя для сервера.", - "generic-reading-list-update": "Возникла проблема с обновлением списка для чтения", - "name-required": "Имя не может быть пустым", - "collection-tag-title-required": "Нужно указать название коллекции", - "invalid-path": "Неверный путь", - "generic-device-update": "При обновлении устройства возникла ошибка", - "invalid-username": "Неверное имя пользователя", - "series-restricted-age-restriction": "Просмотр этой серии запрещён из-за возрастных ограничений", - "user-doesnt-exist": "Пользователь не существует", - "bookmark-permission": "У вас нет прав на добавление/снятие закладок", - "reading-list-position": "Не удалось обновить позицию", - "generic-error": "Что-то пошло не так, пожалуйста, попробуйте еще раз", - "reading-list-doesnt-exist": "Список для чтения не существует", - "file-missing": "Файл не найден в книге", - "send-to-device-status": "Передача файлов на устройство", - "greater-0": "{0} должно быть больше 0", - "collection-deleted": "Коллекция удалена", - "device-doesnt-exist": "Устройство не существует", - "update-metadata-fail": "Не удалось обновить метаданные", - "generic-relationship": "Возникла проблема с обновлением отношений", - "no-series-collection": "Не удалось получить серию для коллекции", - "libraries-restricted": "Пользователь не имеет доступа ни к одной библиотеке", - "no-series": "Не удалось получить серию для библиотеки", - "age-restriction-not-applicable": "Без ограничений", - "series-updated": "Успешно обновлено", - "job-already-running": "Работа уже выполняется", - "generic-series-update": "Возникла ошибка при обновлении серии", - "generic-series-delete": "Возникла проблема с удалением серии", - "theme-doesnt-exist": "Отсутствует или недействителен файл темы", - "scrobble-bad-payload": "Плохая полезная нагрузка от провайдера Scrobble", - "epub-html-missing": "Не удалось найти подходящий html для этой страницы", - "ip-address-invalid": "IP-адрес '{0}' недействителен", - "browse-external-sources": "Обзор внешних источников", - "collections": "Все коллекции", - "url-not-valid": "Url не возвращает действительное изображение или требует авторизации", - "generic-cover-chapter-save": "Невозможно сохранить изображение обложки в главе", - "bad-copy-files-for-download": "Невозможно скопировать файлы в временный каталог архива загрузки.", - "smart-filters": "Умные фильтры", - "browse-smart-filters": "Поиск по Smart Filters", - "epub-malformed": "Файл неправильно сформирован! Невозможно прочитать.", - "opds-disabled": "OPDS не включен на этом сервере", - "stats-permission-denied": "Вы не имеете права просматривать статистику другого пользователя", - "reading-list-restricted": "Список чтения не существует или у вас нет доступа", - "favicon-doesnt-exist": "Favicon не существует", - "external-source-already-in-use": "Существует поток с этим внешним источником", - "issue-num": "Вопрос {0}{1}", - "generic-create-temp-archive": "Возникла проблема с созданием временного архива", - "bookmark-dir-permissions": "Каталог Закладок не имеет правильных разрешений для использования Кавитой", - "total-backups": "Общее количество резервных копий должно быть от 1 до 30", - "book-num": "Книга {0}", - "generic-cover-series-save": "Невозможно сохранить изображение обложки в серии", - "user-no-access-library-from-series": "Пользователь не имеет доступа к библиотеке, к которой принадлежит эта серия", - "volume-num": "Том {0}", - "search-description": "Поиск серий, коллекций или списков для чтения", - "send-to-permission": "Невозможно отправить не-EPUB или PDF на устройства, поскольку они не поддерживаются на Kindle", - "not-authenticated": "Пользователь не аутентифицирован", - "recently-added": "Недавно добавленные", - "chapter-num": "Глава {0}", - "device-not-created": "Этого устройства еще не существует. Пожалуйста, создайте его первым", - "dashboard-stream-doesnt-exist": "Панель потока не существует", - "query-required": "Вы должны передать параметр запроса", - "device-duplicate": "Устройство с таким именем уже существует", - "smart-filter-doesnt-exist": "Умный фильтр не существует", - "on-deck": "На столе", - "anilist-cred-expired": "Срок действия учетных данных AniList истек или они не установлены", - "external-source-already-exists": "Внешний источник уже существует", - "sidenav-stream-doesnt-exist": "Поток SideNav не существует", - "browse-reading-lists": "Просмотреть списки для чтения", - "browse-collections": "Поиск по коллекциям", - "external-source-doesnt-exist": "Внешний источник не существует", - "generic-cover-library-save": "Невозможно сохранить изображение обложки в библиотеке", - "total-logs": "Общее количество журналов должно быть от 1 до 30", - "browse-recently-added": "Просмотреть Недавно добавленные", - "reset-chapter-lock": "Невозможно сбросить блокировку обложки для главы", - "generic-user-delete": "Не удалось удалить пользователя", - "generic-cover-reading-list-save": "Невозможно сохранить изображение обложки в списке для чтения", - "unable-to-register-k+": "Невозможно зарегистрировать лицензию из-за ошибки. Обратитесь в службу поддержки Кавита+", - "encode-as-warning": "Вы не можете конвертировать в формат PNG. Для обновления обложек используйте команду \"Обновить обложку\". Закладки и значки не могут быть закодированы обратно.", - "want-to-read": "Хотите прочитать", - "generic-user-pref": "Возникла проблема с сохранением предпочтений", - "external-sources": "Внешние источники", - "search": "Поиск", - "access-denied": "У вас нет доступа", - "reading-lists": "Списки для чтения", - "url-required": "Вы должны передать url для использования", - "reading-list-title-required": "Заголовок списка чтения не может быть пустым", - "external-source-required": "Требуется ключ ApiKey и хост", - "browse-on-deck": "Просмотреть на столе", - "browse-want-to-read": "Просмотреть Хотите прочитать", - "libraries": "Все библиотеки", - "smart-filter-already-in-use": "Существует поток с этим умным фильтром", - "browse-libraries": "Поиск по библиотекам", - "generic-cover-collection-save": "Невозможно сохранить изображение обложки в Коллекцию", - "progress-must-exist": "Прогресс должен существовать у пользователя", - "browse-more-in-genre": "Посмотреть больше в {0}", - "more-in-genre": "Больше в жанре {0}", - "recently-updated": "Недавно обновленный", - "browse-recently-updated": "Просмотреть недавно обновленные", - "email-not-enabled": "На данном сервере отключена почта. Вы не можете совершить это действие.", - "send-to-unallowed": "Вы не можете отправить на чужое устроиство", - "send-to-size-limit": "Файл(ы) имеют слишком большой вес для отправки по почте", - "check-updates": "Проверьте обновления", - "license-check": "Проверка лицензии", - "process-scrobbling-events": "События, связанные со скроблингом процессов", - "report-stats": "Статистика отчетов", - "cleanup": "Очистка", - "update-yearly-stats": "Обновление статистики за год", - "collection-already-exists": "Коллекция уже существует", - "error-import-stack": "Возникла проблема с импортом стека MAL", - "unable-to-reset-k+": "Невозможно сбросить лицензию Kavita+ из-за ошибки. Обратитесь в службу поддержки Kavita+", - "account-email-invalid": "Адрес электронной почты, указанный в файле для учетной записи администратора, не является действительным. Не удается отправить тестовое электронное письмо.", - "email-settings-invalid": "В настройках электронной почты отсутствует информация. Убедитесь, что все настройки электронной почты сохранены.", - "check-scrobbling-tokens": "Проверьте токены скроблинга", - "backup": "Резервное копирование", - "process-processed-scrobbling-events": "Обработка обработанных событий скроблинга", - "scan-libraries": "Сканирование библиотек", - "kavita+-data-refresh": "Обновление данных Kavita+", - "kavitaplus-restricted": "Это доступно только для Kavita+", - "person-doesnt-exist": "Персона не существует", - "generic-cover-volume-save": "Не удается сохранить обложку для тома", - "generic-cover-person-save": "Не удается сохранить изображение обложки для Персоны", - "person-name-unique": "Имя персоны должно быть уникальным", - "person-image-doesnt-exist": "Персона не существует в CoversDB", - "email-taken": "Почта уже используется", - "person-name-required": "Имя персоны обязательно и не может быть пустым" + "confirm-email": "Сначала вы должны подтвердить свой адрес электронной почты", + "bad-credentials": "Ваши учетные данные неверны" } diff --git a/API/I18N/sk.json b/API/I18N/sk.json index ef267ed02..0967ef424 100644 --- a/API/I18N/sk.json +++ b/API/I18N/sk.json @@ -1,213 +1 @@ -{ - "disabled-account": "Váš účet je deaktivovaný. Kontaktujte správcu servera.", - "register-user": "Niečo sa pokazilo pri registrácii užívateľa", - "confirm-email": "Najprv musíte potvrdiť svoj e-mail", - "locked-out": "Boli ste zamknutí z dôvodu veľkého počtu neúspešných pokusov o prihlásenie. Počkajte 10 minút.", - "validate-email": "Pri validácii vášho e-mailu sa vyskytla chyba: {0}", - "confirm-token-gen": "Pri vytváraní potvrdzovacieho tokenu sa vyskytla chyba", - "permission-denied": "Na vykonanie tejto úlohy nemáte oprávnenie", - "password-required": "Ak nie ste administrátor, musíte na vykonanie zmien vo vašom profile zadať vaše aktuálne heslo", - "invalid-password": "Nesprávne heslo", - "invalid-token": "Nesprávny token", - "unable-to-reset-key": "Niečo sa pokazilo, kľúč nie je možné resetovať", - "invalid-payload": "Nesprávny payload", - "nothing-to-do": "Nič na vykonanie", - "share-multiple-emails": "Nemôžete zdielať e-maily medzi rôznymi účtami", - "generate-token": "Pri generovaní potvrdzovacieho tokenu e-mailu sa vyskytla chyba. Pozrite záznamy udalostí", - "age-restriction-update": "Pri aktualizovaní vekového obmedzenia sa vyskytla chyba", - "no-user": "Používateľ neexistuje", - "generic-user-update": "Aktualizácia používateľa prebehla s výnimkou", - "username-taken": "Používateľské meno už existuje", - "user-already-confirmed": "Používateľ je už potvrdený", - "user-already-registered": "Používateľ je už registrovaný ako {0}", - "user-already-invited": "Používateľ je už pod týmto e-mailom pozvaný a musí ešte prijať pozvanie.", - "generic-password-update": "Pri potvrdení nového hesla sa vyskytla neočakávaná chyba", - "generic-invite-user": "Pri pozývaní tohto používateľa sa vyskytla chyba. Pozrite záznamy udalostí.", - "password-updated": "Heslo aktualizované", - "forgot-password-generic": "E-mail bude odoslaný na zadanú adresu len v prípade, ak existuje v databáze", - "invalid-email-confirmation": "Neplatné potvrdenie e-mailu", - "not-accessible-password": "Váš server nie je dostupný. Odkaz na resetovanie vášho hesla je v záznamoch udalostí", - "email-taken": "Zadaný e-mail už existuje", - "denied": "Nepovolené", - "manual-setup-fail": "Manuálne nastavenie nie je možné dokončiť. Prosím zrušte aktuálny postup a znovu vytvorte pozvánku", - "generic-user-email-update": "Nemožno aktualizovať e-mail používateľa. Skontrolujte záznamy udalostí.", - "email-not-enabled": "E-mail nie je na tomto serveri povolený. Preto túto akciu nemôžete vykonať.", - "collection-updated": "Zbierka bola úspešne aktualizovaná", - "device-doesnt-exist": "Zariadenie neexistuje", - "generic-device-delete": "Pri odstraňovaní zariadenia sa vyskytla chyba", - "greater-0": "{0} musí byť väčší ako 0", - "send-to-size-limit": "Snažíte sa odoslať súbor(y), ktoré sú príliš veľké pre vášho e-mailového poskytovateľa", - "send-to-device-status": "Prenos súborov do vášho zariadenia", - "no-cover-image": "Žiadny prebal", - "must-be-defined": "{0} musí byť definovaný", - "generic-favicon": "Pri získavaní favicon-u domény sa vyskytla chyba", - "no-library-access": "Pozužívateľ nemá prístup do tejto knižnice", - "user-doesnt-exist": "Používateľ neexistuje", - "collection-already-exists": "Zbierka už existuje", - "not-accessible": "Váš server nie je dostupný z vonkajšieho prostredia", - "email-sent": "E-mail odoslaný", - "user-migration-needed": "Uvedený používateľ potrebuje migrovať. Odhláste ho a opäť prihláste na spustenie migrácie", - "generic-invite-email": "Pri opakovanom odosielaní pozývacieho e-mailu sa vyskytla chyba", - "email-settings-invalid": "V nastaveniach e-mailu chýbajú potrebné údaje. Uistite sa, že všetky nastavenia e-mailu sú uložené.", - "chapter-doesnt-exist": "Kapitola neexistuje", - "critical-email-migration": "Počas migrácie e-mailu sa vyskytla chyba. Kontaktujte podporu", - "collection-deleted": "Zbierka bola vymazaná", - "generic-error": "Niečo sa pokazilo, skúste to znova", - "collection-doesnt-exist": "Zbierka neexistuje", - "generic-device-update": "Pri aktualizácii zariadenia sa vyskytla chyba", - "bookmark-doesnt-exist": "Záložka neexistuje", - "person-doesnt-exist": "Osoba neexistuje", - "send-to-kavita-email": "Odoslanie do zariadenia nemôže byť použité bez nastavenia e-amilu", - "send-to-unallowed": "Nemôžete odosielať do zariadenia, ktoré nie je vaše", - "generic-library": "Vyskytla sa kritická chyba. Prosím skúste to opäť.", - "pdf-doesnt-exist": "PDF neexistuje, hoci by malo", - "generic-library-update": "Počas aktualizácie knižnice sa vyskytla kritická chyba.", - "invalid-access": "Neplatný prístup", - "perform-scan": "Prosím, vykonajte opakovaný sken na tejto sérii alebo knižnici", - "generic-read-progress": "Pri ukladaní aktuálneho stavu sa vyskytla chyba", - "generic-clear-bookmarks": "Záložky nie je možné vymazať", - "bookmark-permission": "Nemáte oprávnenie na vkladanie/odstraňovanie záložiek", - "bookmark-save": "Nemožno uložiť záložku", - "bookmarks-empty": "Záložky nemôžu byť prázdne", - "library-doesnt-exist": "Knižnica neexistuje", - "invalid-path": "Neplatné umiestnenie", - "generic-send-to": "Pri odosielaní súboru(-ov) do vášho zariadenia sa vyskytla chyba", - "no-image-for-page": "Žiadny taký obrázok pre stránku {0}. Pokúste sa ju obnoviť, aby ste ju mohli nanovo uložiť.", - "delete-library-while-scan": "Nemôžete odstrániť knižnicu počas prebiehajúceho skenovania. Prosím, vyčkajte na dokončenie skenovania alebo reštartujte Kavitu a skúste ju opäť odstrániť", - "invalid-username": "Neplatné používateľské meno", - "account-email-invalid": "E-mail uvedený v údajoch administrátora nie je platným e-mailom. Nie je možné zaslať testovací e-mail.", - "admin-already-exists": "Administrátor už existuje", - "invalid-filename": "Neplatný názov súboru", - "file-doesnt-exist": "Súbor neexistuje", - "invalid-email": "E-mail v záznamoch pre používateľov nie platný e-mail. Odkazy sú uvedené v záznamoch udalostí.", - "file-missing": "Súbor nebol nájdený v knihe", - "error-import-stack": "Pri importovaní MAL balíka sa vyskytla chyba", - "person-name-required": "Meno osoby je povinné a nesmie byť prázdne", - "person-name-unique": "Meno osoby musí byť jedinečné", - "person-image-doesnt-exist": "Osoba neexistuje v databáze CoversDB", - "generic-device-create": "Pri vytváraní zariadenia sa vyskytla chyba", - "series-doesnt-exist": "Séria neexistuje", - "volume-doesnt-exist": "Zväzok neexistuje", - "library-name-exists": "Názov knižnice už existuje. Prosím, vyberte si pre daný server jedinečný názov.", - "cache-file-find": "Nepodarilo sa nájsť obrázok vo vyrovnávacej pamäti. Znova načítajte a skúste to znova.", - "name-required": "Názov nemôže byť prázdny", - "valid-number": "Musí to byť platné číslo strany", - "duplicate-bookmark": "Duplicitný záznam záložky už existuje", - "reading-list-permission": "Nemáte povolenia na tento zoznam na čítanie alebo zoznam neexistuje", - "reading-list-position": "Nepodarilo sa aktualizovať pozíciu", - "reading-list-updated": "Aktualizované", - "reading-list-item-delete": "Položku(y) sa nepodarilo odstrániť", - "reading-list-deleted": "Zoznam na čítanie bol odstránený", - "generic-reading-list-delete": "Pri odstraňovaní zoznamu na čítanie sa vyskytol problém", - "generic-reading-list-update": "Pri aktualizácii zoznamu na čítanie sa vyskytol problém", - "generic-reading-list-create": "Pri vytváraní zoznamu na čítanie sa vyskytol problém", - "reading-list-doesnt-exist": "Zoznam na čítanie neexistuje", - "series-restricted": "Používateľ nemá prístup k tejto sérii", - "generic-scrobble-hold": "Pri pauznutí funkcie sa vyskytla chyba", - "libraries-restricted": "Používateľ nemá prístup k žiadnym knižniciam", - "no-series": "Nepodarilo sa získať sériu pre knižnicu", - "no-series-collection": "Nepodarilo sa získať sériu pre kolekciu", - "generic-series-delete": "Pri odstraňovaní série sa vyskytol problém", - "generic-series-update": "Pri aktualizácii série sa vyskytla chyba", - "series-updated": "Úspešne aktualizované", - "update-metadata-fail": "Nepodarilo sa aktualizovať metadáta", - "age-restriction-not-applicable": "Bez obmedzenia", - "generic-relationship": "Pri aktualizácii vzťahov sa vyskytol problém", - "job-already-running": "Úloha už beží", - "encode-as-warning": "Nedá sa konvertovať do formátu PNG. Pre obaly použite možnosť Obnoviť obaly. Záložky a favicony sa nedajú spätne zakódovať.", - "ip-address-invalid": "IP adresa „{0}“ je neplatná", - "bookmark-dir-permissions": "Adresár záložiek nemá správne povolenia pre použitie v aplikácii Kavita", - "total-backups": "Celkový počet záloh musí byť medzi 1 a 30", - "total-logs": "Celkový počet protokolov musí byť medzi 1 a 30", - "stats-permission-denied": "Nemáte oprávnenie zobraziť si štatistiky iného používateľa", - "url-not-valid": "URL nevracia platný obrázok alebo vyžaduje autorizáciu", - "url-required": "Na použitie musíte zadať URL adresu", - "generic-cover-series-save": "Obrázok obálky sa nepodarilo uložiť do série", - "generic-cover-collection-save": "Obrázok obálky sa nepodarilo uložiť do kolekcie", - "generic-cover-reading-list-save": "Obrázok obálky sa nepodarilo uložiť do zoznamu na čítanie", - "generic-cover-chapter-save": "Obrázok obálky sa nepodarilo uložiť do kapitoly", - "generic-cover-library-save": "Obrázok obálky sa nepodarilo uložiť do knižnice", - "generic-cover-person-save": "Obrázok obálky sa nepodarilo uložiť k tejto osobe", - "generic-cover-volume-save": "Obrázok obálky sa nepodarilo uložiť do zväzku", - "access-denied": "Nemáte prístup", - "reset-chapter-lock": "Nepodarilo sa resetovať zámok obalu pre kapitolu", - "generic-user-delete": "Používateľa sa nepodarilo odstrániť", - "generic-user-pref": "Pri ukladaní predvolieb sa vyskytol problém", - "opds-disabled": "OPDS nie je na tomto serveri povolený", - "on-deck": "Pokračovať v čítaní", - "browse-on-deck": "Prehliadať pokračovanie v čítaní", - "recently-added": "Nedávno pridané", - "want-to-read": "Chcem čítať", - "browse-want-to-read": "Prehliadať Chcem si prečítať", - "browse-recently-added": "Prehliadať nedávno pridané", - "reading-lists": "Zoznamy na čítanie", - "browse-reading-lists": "Prehliadať podľa zoznamov na čítanie", - "libraries": "Všetky knižnice", - "browse-libraries": "Prehliadať podľa knižníc", - "collections": "Všetky kolekcie", - "browse-collections": "Prehliadať podľa kolekcií", - "more-in-genre": "Viac v žánri {0}", - "browse-more-in-genre": "Prezrite si viac v {0}", - "recently-updated": "Nedávno aktualizované", - "browse-recently-updated": "Prehliadať nedávno aktualizované", - "smart-filters": "Inteligentné filtre", - "external-sources": "Externé zdroje", - "browse-external-sources": "Prehliadať externé zdroje", - "browse-smart-filters": "Prehliadať podľa inteligentných filtrov", - "reading-list-restricted": "Zoznam na čítanie neexistuje alebo k nemu nemáte prístup", - "query-required": "Musíte zadať parameter dopytu", - "search": "Hľadať", - "search-description": "Vyhľadávanie sérií, zbierok alebo zoznamov na čítanie", - "favicon-doesnt-exist": "Favicon neexistuje", - "smart-filter-doesnt-exist": "Inteligentný filter neexistuje", - "smart-filter-already-in-use": "Existuje existujúci stream s týmto inteligentným filtrom", - "dashboard-stream-doesnt-exist": "Stream dashboardu neexistuje", - "sidenav-stream-doesnt-exist": "SideNav Stream neexistuje", - "external-source-already-exists": "Externý zdroj už existuje", - "external-source-required": "Vyžaduje sa kľúč API a Host", - "external-source-doesnt-exist": "Externý zdroj neexistuje", - "external-source-already-in-use": "S týmto externým zdrojom existuje stream", - "sidenav-stream-only-delete-smart-filter": "Z bočného panela SideNav je možné odstrániť iba streamy inteligentných filtrov", - "dashboard-stream-only-delete-smart-filter": "Z ovládacieho panela je možné odstrániť iba streamy inteligentných filtrov", - "smart-filter-name-required": "Názov inteligentného filtra je povinný", - "smart-filter-system-name": "Nemôžete použiť názov streamu poskytnutého systémom", - "not-authenticated": "Používateľ nie je overený", - "unable-to-register-k+": "Licenciu sa nepodarilo zaregistrovať z dôvodu chyby. Kontaktujte podporu Kavita+", - "unable-to-reset-k+": "Licenciu Kavita+ sa nepodarilo resetovať z dôvodu chyby. Kontaktujte podporu Kavita+", - "anilist-cred-expired": "Prihlasovacie údaje AniList vypršali alebo chýbajú", - "scrobble-bad-payload": "Nesprávne údaje od poskytovateľa Scrobblovania", - "theme-doesnt-exist": "Súbor témy chýba alebo je neplatný", - "bad-copy-files-for-download": "Súbory sa nepodarilo skopírovať do dočasného adresára na stiahnutie archívu.", - "generic-create-temp-archive": "Pri vytváraní dočasného archívu sa vyskytla chyba", - "epub-malformed": "Súbor je nesprávne naformátovaný! Nedá sa prečítať.", - "epub-html-missing": "Zodpovedajúci súbor HTML pre túto stránku sa nenašiel", - "collection-tag-title-required": "Názov kolekcie nemôže byť prázdny", - "reading-list-title-required": "Názov zoznamu na čítanie nemôže byť prázdny", - "collection-tag-duplicate": "Kolekcia s týmto názvom už existuje", - "device-duplicate": "Zariadenie s týmto názvom už existuje", - "device-not-created": "Toto zariadenie ešte neexistuje. Najprv ho vytvorte", - "send-to-permission": "Nie je možné odoslať súbory iné ako EPUB alebo PDF na zariadenia, pretože nie sú podporované na Kindle", - "progress-must-exist": "Pokrok musí byť u používateľa k dispozícii", - "reading-list-name-exists": "Zoznam na prečítanie s týmto menom už existuje", - "user-no-access-library-from-series": "Používateľ nemá prístup do knižnice, do ktorej táto séria patrí", - "series-restricted-age-restriction": "Používateľ si nemôže pozrieť túto sériu z dôvodu vekového obmedzenia", - "kavitaplus-restricted": "Toto je obmedzené iba na Kavita+", - "aliases-have-overlap": "Jeden alebo viacero aliasov sa prekrýva s inými osobami, nie je možné ich aktualizovať", - "volume-num": "Zväzok {0}", - "book-num": "Kniha {0}", - "issue-num": "Problém {0}{1}", - "chapter-num": "Kapitola {0}", - "check-updates": "Skontrolovať aktualizácie", - "license-check": "Kontrola licencie", - "process-scrobbling-events": "Udalosti procesu scrobblovania", - "report-stats": "Štatistiky hlásení", - "check-scrobbling-tokens": "Skontrolujte Tokeny Scrobblingu", - "cleanup": "Čistenie", - "process-processed-scrobbling-events": "Spracovať udalosti scrobblovania", - "remove-from-want-to-read": "Upratanie listu Chcem si prečítať", - "scan-libraries": "Skenovanie knižníc", - "kavita+-data-refresh": "Obnovenie údajov Kavita+", - "backup": "Záloha", - "update-yearly-stats": "Aktualizovať ročné štatistiky", - "generated-reading-profile-name": "Vygenerované z {0}" -} +{} diff --git a/API/I18N/sl.json b/API/I18N/sl.json deleted file mode 100644 index 0967ef424..000000000 --- a/API/I18N/sl.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/API/I18N/sv.json b/API/I18N/sv.json deleted file mode 100644 index 64004a7a8..000000000 --- a/API/I18N/sv.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "disabled-account": "Ditt konto är inaktiverat. Kontakta serveradministratören.", - "register-user": "Någonting gick fel vid registrering av användare", - "validate-email": "Det uppstod ett problem vid bekräftelsen av din e-post: {0}", - "denied": "Inte tillåtet", - "permission-denied": "Du har inte tillåtelse att utföra den här operationen", - "age-restriction-update": "Det uppstod ett fel vid uppdatering av åldersbegränsningen", - "no-user": "Användaren finns inte", - "username-taken": "Användarnamnet är redan upptaget", - "user-already-confirmed": "Användaren är redan bekräftad", - "generic-user-update": "Det var en avvikelse vid uppdatering av användaren", - "manual-setup-fail": "Manuell inställning kan inte bli slutföras. Vänligen avbryt och återskapa inbjudan", - "user-already-registered": "Användare är redan registrerad som {0}", - "forgot-password-generic": "Ett mail skickas till e-posten om det finns i vår databas", - "not-accessible-password": "Din server är inte tillgänglig. Länken för att återställa ditt lösenord finns i loggarna", - "invalid-email": "E-posten i arkivet för användaren är inte en giltig e-post. Se loggar för länkar.", - "not-accessible": "Din server är inte tillgänglig externt", - "email-sent": "Mail skickat", - "user-migration-needed": "Denna användare behöver migreras. Se till att de loggar ut och in igen för att trigga migrationsprocessen", - "generic-invite-email": "Det uppstod ett problem med att skicka inbjudningsmail igen", - "admin-already-exists": "Administratör finns redan", - "invalid-username": "Ogiltigt användarnamn", - "critical-email-migration": "Det uppstod ett problem vid e-postmigration. Kontakta supporten", - "email-not-enabled": "E-post är inte aktiverat på denna server. Du kan inte utföra den här åtgärden.", - "account-email-invalid": "E-posten i arkivet för användaren är inte en giltig e-post. Kan inte skicka testmail.", - "email-settings-invalid": "E-postinställningarna saknar information. Se till att alla e-postinställningar är sparade.", - "chapter-doesnt-exist": "Kapitel saknas", - "collection-updated": "Samlingen har uppdaterats", - "collection-deleted": "Samling borttagen", - "collection-doesnt-exist": "Samling finns inte", - "collection-already-exists": "Samling finns redan", - "error-import-stack": "Det uppstod ett fel vid import av MAL stack", - "generic-device-create": "Det uppstod ett fel vid skapandet av enheten", - "generic-device-update": "Det uppstod ett fel vid uppdatering av enheten", - "generic-device-delete": "Det uppstod ett fel vid borttagning av enheten", - "greater-0": "{0} måste vara större än 0", - "send-to-kavita-email": "Skicka till enhet kan inte användas utan inställning av e-post", - "send-to-size-limit": "Filerna du försöker skicka är för stora för din e-postleverantör", - "generic-send-to": "Det uppstod ett fel vid sändningen av dina filer till enheten", - "bookmarks-empty": "Bokmärken kan inte vara tomma", - "series-doesnt-exist": "Serie saknas", - "volume-doesnt-exist": "Volym saknas", - "must-be-defined": "{0} måste vara definierad", - "generic-favicon": "Det uppstod ett problem vid hämtning av favikon för domän", - "invalid-filename": "Ogiltigt filnamn", - "file-doesnt-exist": "Fil saknas", - "library-name-exists": "Biblioteksnamn finns redan. Vänligen välj ett unikt namn för servern.", - "generic-library": "Det uppstod ett kritiskt fel. Vänligen försök igen.", - "no-library-access": "Användaren har inte tillgång till detta bibliotek", - "user-doesnt-exist": "Användaren finns inte", - "library-doesnt-exist": "Biblioteket finns inte", - "invalid-path": "Ogiltig Sökväg", - "generic-library-update": "Det uppstod ett kritiskt fel vid uppdatering av biblioteket.", - "pdf-doesnt-exist": "PDF saknas när den borde finnas", - "invalid-access": "Ogiltig Åtkomst", - "no-image-for-page": "Ingen bild för sidan {0}. Försök att uppdatera för att tillåta återcache.", - "generic-read-progress": "Ett problem uppstod vid sparandet av historik", - "generic-clear-bookmarks": "Kunde inte rensa bokmärken", - "bookmark-save": "Kunde inte spara bokmärke", - "cache-file-find": "Kunde inte hitta cachad bild. Ladda om och försök igen.", - "name-required": "Namn kan inte vara tomt", - "valid-number": "Måste vara ett giltigt sidnummer", - "duplicate-bookmark": "Bokmärkespost med samma namn finns redan", - "reading-list-position": "Kunde inte uppdatera position", - "reading-list-item-delete": "Kunde inte ta bort objekt", - "reading-list-deleted": "Läslista borttagen", - "generic-reading-list-delete": "Ett problem uppstod vid borttagning av läslistan", - "generic-reading-list-update": "Ett problem uppstod vid uppdatering av läslistan", - "reading-list-doesnt-exist": "Läslista saknas", - "series-restricted": "Användaren har inte behörighet för denna Serien", - "generic-scrobble-hold": "Ett fel uppstod när uppehållet lades till", - "libraries-restricted": "Användaren har inte behörighet till några bibliotek", - "no-series": "Kunde inte skaffa serier för Bibliotek", - "generic-series-delete": "Det uppstod ett problem vid borttagning av serien", - "generic-series-update": "Det uppstod ett fel vid uppdatering av serien", - "series-updated": "Uppdatering lyckades", - "update-metadata-fail": "Kunde inte uppdatera metadata", - "age-restriction-not-applicable": "Ingen Begränsning", - "job-already-running": "Jobb körs redan", - "ip-address-invalid": "IP Adress '{0}' är ogiltig", - "bookmark-dir-permissions": "Bokmärkeskatalogen har inte rätt behörigheter för Kavita att använda", - "stats-permission-denied": "Du har inte behörighet att se andra användares statistik", - "url-not-valid": "Url returnerar inte en giltig bild eller kräver auktorisering", - "url-required": "Du måste skicka en url att använda", - "generic-cover-series-save": "Det gick inte att spara omslagsbilden till Serier", - "generic-cover-collection-save": "Det gick inte att spara omslagsbilden till Samling", - "generic-cover-reading-list-save": "Det gick inte att spara omslagsbilden till Läslista", - "generic-cover-chapter-save": "Det gick inte att spara omslagsbilden till Kapitel", - "generic-cover-library-save": "Det gick inte att spara omslagsbilden till Bibliotek", - "access-denied": "Du saknar behörighet", - "reset-chapter-lock": "Det gick inte att återställa omslagsbilden för Kapitel", - "generic-user-delete": "Kunde inte ta bort användaren", - "generic-user-pref": "Det uppstod ett problem vid sparandet av inställningar", - "recently-added": "Nyligen Tillagt", - "want-to-read": "Vill Läsa", - "browse-want-to-read": "Bläddra i Vill Läsa", - "browse-recently-added": "Bläddra i Nyligen Tillagt", - "reading-lists": "Läslistor", - "libraries": "Alla Bibliotek", - "browse-libraries": "Bläddra efter Bibliotek", - "recently-updated": "Nyligen Uppdaterat", - "browse-recently-updated": "Bläddra i Nyligen Uppdaterat", - "smart-filters": "Smarta Filter", - "external-sources": "Externa Källor", - "browse-external-sources": "Bläddra i Externa Källor", - "browse-smart-filters": "Bläddra efter Smarta Filter", - "reading-list-restricted": "Läslista saknas eller så saknar du behörighet", - "query-required": "Du måste ange en sökterm", - "search": "Sök", - "search-description": "Sök efter Serier, Samlingar, eller Läslistor", - "favicon-doesnt-exist": "Favikon saknas", - "smart-filter-doesnt-exist": "Smart Filter saknas", - "smart-filter-already-in-use": "Det finns redan en ström med detta Smarta Filter", - "external-source-already-exists": "Extern Källa finns redan", - "external-source-required": "ApiKey och Host krävs", - "external-source-doesnt-exist": "Extern Källa saknas", - "external-source-already-in-use": "Det finns redan en stream med denna Externa Källa", - "not-authenticated": "Användaren är inte autentiserad", - "unable-to-reset-k+": "Det gick inte att återställa Kavita+-licensen på grund av ett fel. Kontakta Kavita+ Support", - "anilist-cred-expired": "AniList Autentiseringsuppgifter har gått ut eller är inte angivna", - "scrobble-bad-payload": "Felaktig payload från Scrobble utgivare", - "theme-doesnt-exist": "Temafil saknas eller är ogiltig", - "generic-create-temp-archive": "Det uppstod ett problem vid skapandet av tillfällig katalog", - "epub-malformed": "Filen är felaktigt utformad! Kan inte läsa.", - "collection-tag-title-required": "Samlingstitel kan inte vara tom", - "reading-list-title-required": "Läsliststitel kan inte vara tom", - "collection-tag-duplicate": "En samling med detta namn finns redan", - "device-duplicate": "En enhet med detta namn finns redan", - "reading-list-name-exists": "En läslista med detta namn finns redan", - "user-no-access-library-from-series": "Användaren har inte tillgång till biblioteket som denna serie tillhör", - "volume-num": "Volym {0}", - "book-num": "Bok {0}", - "issue-num": "Utgåva {0}{1}", - "chapter-num": "Kapitel {0}", - "check-updates": "Kontrollera Uppdateringar", - "license-check": "Licenskontroll", - "kavita+-data-refresh": "Uppdatera Kavita+ Data", - "backup": "Backup", - "update-yearly-stats": "Uppdatera Årlig Statistik", - "confirm-email": "Du måste bekräfta din e-post först", - "locked-out": "Du har blivit utelåst på grund av för många auktoriseringsförsök. Vänligen vänta 10 minuter.", - "confirm-token-gen": "Det uppstod ett problem med att generera en bekräftelsetoken", - "password-required": "Du måste ange ditt existerande lösenord för att kunna ändra ditt konto om du inte är administratör", - "invalid-password": "Ogiltigt Lösenord", - "invalid-token": "Ogiltig token", - "unable-to-reset-key": "Någonting gick fel, kan inte återställa nyckeln", - "nothing-to-do": "Inget att göra", - "invalid-payload": "Ogiltig payload", - "share-multiple-emails": "Du kan inte dela e-post mellan flera konton", - "generate-token": "Det uppstod ett problem med att generera en e-postbekräftelsetoken. Se loggar", - "user-already-invited": "Användaren är redan inbjuden med denna e-post och har ännu inte accepterat inbjudan.", - "generic-invite-user": "Det uppstod ett problem vid inbjudan av användaren. Vänligen kontrollera loggarna.", - "invalid-email-confirmation": "Ogiltig e-postbekräftelse", - "generic-user-email-update": "Kan inte uppdatera e-post för användaren. Kontrollera loggarna.", - "generic-password-update": "Det uppstod ett oväntat fel vid bekräftelse av nytt lösenord", - "password-updated": "Lösenord Uppdaterat", - "file-missing": "Fil kunde ej hittas i bok", - "generic-error": "Någonting gick fel, vänligen försök igen", - "device-doesnt-exist": "Enheten finns inte", - "send-to-unallowed": "Du kan inte skicka till en enhet som inte är din", - "send-to-device-status": "Överför filer till din enhet", - "no-cover-image": "Omslagsbild saknas", - "bookmark-doesnt-exist": "Bokmärke saknas", - "delete-library-while-scan": "Du kan inte ta bort ett bibliotek medan en skanning pågår. Vänligen vänta tills skanningen är färdig eller starta om Kavita för att försöka ta bort igen", - "perform-scan": "Vänligen utför en skanning av denna serie eller bibliotek och försök igen", - "bookmark-permission": "Du har inte behörighet att bokmärka/ta bort bokmärke", - "reading-list-permission": "Du har inte behörighet för den här läslistan eller så finns listan inte", - "reading-list-updated": "Uppdaterat", - "generic-reading-list-create": "Ett problem uppstod vid skapandet av läslistan", - "encode-as-warning": "Du kan inte konvertera till PNG. För omslag, använd Uppdatera Omslag. Bokmärken och favikoner kan inte kodas tillbaka.", - "no-series-collection": "Kunde inte skaffa serier för Samling", - "generic-relationship": "Det uppstod ett problem vid uppdatering av förhållanden", - "total-backups": "Totala Säkerhetskopior måste vara mellan 1 och 30", - "browse-reading-lists": "Bläddra i Läslistor", - "opds-disabled": "OPDS är inte aktiverat på denna server", - "total-logs": "Totala Loggar måste vara mellan 1 och 30", - "collections": "Alla Samlingar", - "browse-collections": "Bläddra efter Samlingar", - "more-in-genre": "Mer i Genre {0}", - "browse-more-in-genre": "Bläddra mer i {0}", - "unable-to-register-k+": "Kan inte registrera licens på grund av ett fel. Kontakta Kavita+ Support", - "bad-copy-files-for-download": "Kan inte kopiera filer till temp-katalogen för arkivnedladdning.", - "epub-html-missing": "Kunde inte hitta lämplig html för den sidan", - "device-not-created": "Denna enhet finns inte än. Vänligen skapa den först", - "series-restricted-age-restriction": "Användaren saknar behörighet att se denna serien på grund av åldersbegränsning", - "send-to-permission": "Kan inte skicka icke-EPUB eller PDF till enheter som inte stöds av Kindle", - "process-processed-scrobbling-events": "Processera Processerade Scrobbling Event", - "remove-from-want-to-read": "Vill Läsa Städning", - "process-scrobbling-events": "Processera Scrobbling Event", - "scan-libraries": "Skanna Bibliotek", - "report-stats": "Rapportera Statistik", - "progress-must-exist": "Historik måste finnas på användare", - "check-scrobbling-tokens": "Kontrollera Scrobbling Tokens", - "cleanup": "Städning", - "on-deck": "Fortsätt Läsa", - "browse-on-deck": "Bläddra i Fortsätt Läsa", - "dashboard-stream-doesnt-exist": "Dashboard Ström saknas", - "sidenav-stream-doesnt-exist": "SideNav Ström saknas", - "person-doesnt-exist": "Personen existerar inte", - "person-name-required": "Personnamn är obligatorisk", - "person-name-unique": "Personnamn måste vara unikt", - "person-image-doesnt-exist": "Personen existerar inte i CoversDB", - "generic-cover-person-save": "Kan inte spara omslagsbilden till Personen", - "generic-cover-volume-save": "Kan inte spara omslagsbilden till Volymen", - "email-taken": "E-postadressen används redan", - "kavitaplus-restricted": "Detta är enbart tillgängligt med Kavita+", - "dashboard-stream-only-delete-smart-filter": "Bara smarta-filter-strömmar kan tas bort från panelen", - "smart-filter-name-required": "Smart Filter namn krävs", - "sidenav-stream-only-delete-smart-filter": "Bara smarta-filter-strömmar kan tas bort från sidomenyn", - "smart-filter-system-name": "Du kan inte använda namnet från en ström som systemet tillhandahåller" -} diff --git a/API/I18N/ta.json b/API/I18N/ta.json deleted file mode 100644 index cc20ab40f..000000000 --- a/API/I18N/ta.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "generic-invite-user": "பயனரை அழைக்கும் சிக்கல் இருந்தது. பதிவுகளை சரிபார்க்கவும்.", - "user-already-invited": "பயனர் ஏற்கனவே இந்த மின்னஞ்சலின் கீழ் அழைக்கப்படுகிறார், மேலும் அழைப்பை இன்னும் ஏற்கவில்லை.", - "invalid-email-confirmation": "தவறான மின்னஞ்சல் உறுதிப்படுத்தல்", - "generic-user-email-update": "பயனருக்கான மின்னஞ்சலைப் புதுப்பிக்க முடியவில்லை. பதிவுகளை சரிபார்க்கவும்.", - "generic-password-update": "புதிய கடவுச்சொல்லை உறுதிப்படுத்தும்போது எதிர்பாராத பிழை ஏற்பட்டது", - "password-updated": "கடவுச்சொல் புதுப்பிக்கப்பட்டது", - "forgot-password-generic": "எங்கள் தரவுத்தளத்தில் இருந்தால் மின்னஞ்சல் அனுப்பப்படும்", - "bad-copy-files-for-download": "கோப்புகளை தற்காலிக அடைவு காப்பக பதிவிறக்கத்திற்கு நகலெடுக்க முடியவில்லை.", - "generic-create-temp-archive": "தற்காலிக காப்பகத்தை உருவாக்கும் சிக்கல் இருந்தது", - "epub-malformed": "கோப்பு தவறாக உள்ளது! படிக்க முடியாது.", - "epub-html-missing": "அந்தப் பக்கத்திற்கு பொருத்தமான உஉகுமொ ஐக் கண்டுபிடிக்க முடியவில்லை", - "collection-tag-title-required": "சேகரிப்பு தலைப்பு காலியாக இருக்க முடியாது", - "collection-tag-duplicate": "இந்த பெயருடன் ஒரு தொகுப்பு ஏற்கனவே உள்ளது", - "device-duplicate": "இந்த பெயரைக் கொண்ட ஒரு சாதனம் ஏற்கனவே உள்ளது", - "not-accessible-password": "உங்கள் சேவையகம் அணுக முடியாது. உங்கள் கடவுச்சொல்லை மீட்டமைப்பதற்கான இணைப்பு பதிவுகளில் உள்ளது", - "device-not-created": "இந்த சாதனம் இன்னும் இல்லை. முதலில் உருவாக்கவும்", - "send-to-permission": "கின்டலில் ஆதரிக்கப்படாத சாதனங்களுக்கு எபப் அல்லாத அல்லது பி.டி.எஃப் அனுப்ப முடியாது", - "progress-must-exist": "பயனரில் முன்னேற்றம் இருக்க வேண்டும்", - "reading-list-name-exists": "இந்த பெயரின் வாசிப்பு பட்டியல் ஏற்கனவே உள்ளது", - "user-no-access-library-from-series": "பயனருக்கு நூலகத்திற்கு அணுகல் இல்லை இந்த தொடர் சொந்தமானது", - "series-restricted-age-restriction": "அகவை கட்டுப்பாடுகள் காரணமாக இந்தத் தொடரைப் பார்க்க பயனருக்கு இசைவு இல்லை", - "volume-num": "தொகுதி {0}", - "book-num": "நூல் {0}", - "issue-num": "வெளியீடு {0} {1}", - "chapter-num": "அத்தியாயம் {0}", - "check-updates": "புதுப்பிப்புகளை சரிபார்க்கவும்", - "license-check": "உரிம சோதனை", - "process-scrobbling-events": "செயலாக்க நிகழ்வுகளை செயலாக்கவும்", - "report-stats": "புள்ளிவிவரங்களைப் புகாரளிக்கவும்", - "check-scrobbling-tokens": "ச்க்ரோப்ளிங் டோக்கன்களை சரிபார்க்கவும்", - "cleanup": "தூய்மைப்படுத்துதல்", - "process-processed-scrobbling-events": "செயல்முறை செயலாக்கப்பட்ட ச்க்ரோப்லிங் நிகழ்வுகள்", - "remove-from-want-to-read": "தூய்மைப்படுத்தலைப் படிக்க விரும்புகிறேன்", - "scan-libraries": "நூலகங்களை ச்கேன் செய்யுங்கள்", - "kavita+-data-refresh": "கவிதா+ தரவு புதுப்பிப்பு", - "backup": "காப்புப்பிரதி", - "update-yearly-stats": "ஆண்டு புள்ளிவிவரங்களைப் புதுப்பிக்கவும்", - "invalid-email": "பயனருக்கான கோப்பில் உள்ள மின்னஞ்சல் சரியான மின்னஞ்சல் அல்ல. எந்த இணைப்புகளுக்கான பதிவுகளையும் காண்க.", - "not-accessible": "உங்கள் சேவையகம் வெளிப்புறமாக அணுக முடியாது", - "email-sent": "மின்னஞ்சல் அனுப்பப்பட்டது", - "user-migration-needed": "இந்த பயனர் இடம்பெயர வேண்டும். இடம்பெயர்வு ஓட்டத்தைத் தூண்டுவதற்கு அவை வெளியேறி உள்நுழைய வேண்டும்", - "generic-invite-email": "அழைப்பு மின்னஞ்சல் மீண்டும் ஒரு சிக்கல் இருந்தது", - "admin-already-exists": "நிர்வாகி ஏற்கனவே உள்ளது", - "invalid-username": "தவறான பயனர்பெயர்", - "critical-email-migration": "மின்னஞ்சல் இடம்பெயர்வு போது ஒரு சிக்கல் இருந்தது. தொடர்பு உதவி", - "email-not-enabled": "இந்த சேவையகத்தில் மின்னஞ்சல் இயக்கப்படவில்லை. இந்த செயலை நீங்கள் செய்ய முடியாது.", - "must-be-defined": "{0} வரையறுக்கப்பட வேண்டும்", - "generic-favicon": "டொமைனுக்கு ஃபேவிகானைப் பெறுவதில் சிக்கல் இருந்தது", - "invalid-filename": "தவறான கோப்பு பெயர்", - "file-doesnt-exist": "கோப்பு இல்லை", - "library-name-exists": "நூலக பெயர் ஏற்கனவே உள்ளது. சேவையகத்திற்கு ஒரு தனிப்பட்ட பெயரைத் தேர்வுசெய்க.", - "generic-library": "ஒரு முக்கியமான சிக்கல் இருந்தது. மீண்டும் முயற்சிக்கவும்.", - "no-library-access": "பயனருக்கு இந்த நூலகத்திற்கு அணுகல் இல்லை", - "library-doesnt-exist": "நூலகம் இல்லை", - "invalid-path": "தவறான பாதை", - "no-series-collection": "சேகரிப்புக்கான தொடர்களைப் பெற முடியவில்லை", - "generic-series-delete": "தொடரை நீக்குவதில் சிக்கல் இருந்தது", - "generic-series-update": "தொடரைப் புதுப்பிப்பதில் பிழை ஏற்பட்டது", - "series-updated": "வெற்றிகரமாக புதுப்பிக்கப்பட்டது", - "update-metadata-fail": "மெட்டாடேட்டாவை புதுப்பிக்க முடியவில்லை", - "age-restriction-not-applicable": "கட்டுப்பாடு இல்லை", - "generic-relationship": "உறவுகளைப் புதுப்பிப்பதில் சிக்கல் இருந்தது", - "job-already-running": "ஏற்கனவே இயங்கும் வேலை", - "browse-reading-lists": "பட்டியல்களைப் படிப்பதன் மூலம் உலாவுக", - "libraries": "அனைத்து நூலகங்களும்", - "browse-libraries": "நூலகங்களால் உலாவுக", - "collections": "அனைத்து சேகரிப்புகளும்", - "browse-collections": "வசூல் மூலம் உலாவுக", - "more-in-genre": "{0} வகைகளில் மேலும்", - "browse-more-in-genre": "{0} இல் மேலும் உலாவுக", - "recently-updated": "அண்மைக் காலத்தில் புதுப்பிக்கப்பட்டது", - "browse-recently-updated": "உலாவு அண்மைக் காலத்தில் புதுப்பிக்கப்பட்டது", - "smart-filters": "அறிவுள்ள வடிப்பான்கள்", - "external-sources": "வெளிப்புற ஆதாரங்கள்", - "browse-external-sources": "வெளிப்புற ஆதாரங்களை உலாவுக", - "browse-smart-filters": "அறிவுள்ள வடிப்பான்களால் உலாவுக", - "reading-list-restricted": "வாசிப்பு பட்டியல் இல்லை அல்லது உங்களுக்கு அணுகல் இல்லை", - "query-required": "நீங்கள் ஒரு வினவல் அளவுருவை அனுப்ப வேண்டும்", - "search-description": "தொடர், தொகுப்புகள் அல்லது வாசிப்பு பட்டியல்களைத் தேடுங்கள்", - "favicon-doesnt-exist": "ஃபாவிகான் இல்லை", - "smart-filter-doesnt-exist": "அறிவுள்ள வடிகட்டி இல்லை", - "smart-filter-already-in-use": "இந்த அறிவுள்ள வடிகட்டியுடன் ஏற்கனவே ச்ட்ரீம் உள்ளது", - "dashboard-stream-doesnt-exist": "டாச்போர்டு ச்ட்ரீம் இல்லை", - "sidenav-stream-doesnt-exist": "சிடெனவ் ச்ட்ரீம் இல்லை", - "external-source-already-exists": "வெளிப்புற மூலமானது ஏற்கனவே உள்ளது", - "external-source-required": "அப்பிகி மற்றும் புரவலன் தேவை", - "external-source-doesnt-exist": "வெளிப்புற மூல இல்லை", - "external-source-already-in-use": "இந்த வெளிப்புற மூலத்துடன் ஏற்கனவே ச்ட்ரீம் உள்ளது", - "not-authenticated": "பயனர் அங்கீகரிக்கப்படவில்லை", - "unable-to-register-k+": "பிழை காரணமாக உரிமத்தை பதிவு செய்ய முடியவில்லை. கவிதா+ ஆதரவை அணுகவும்", - "unable-to-reset-k+": "Unable பெறுநர் மீட்டமை Kavita+ உரிமம் due பெறுநர் error. கவிதா+ ஆதரவை அணுகவும்", - "anilist-cred-expired": "அனிலிச்ட் நற்சான்றிதழ்கள் காலாவதியானன அல்லது அமைக்கப்படவில்லை", - "scrobble-bad-payload": "ச்க்ரோபல் வழங்குநரிடமிருந்து மோசமான பேலோட்", - "theme-doesnt-exist": "கருப்பொருள் கோப்பு காணவில்லை அல்லது தவறானது", - "search": "தேடல்", - "age-restriction-update": "அகவை கட்டுப்பாட்டை புதுப்பிப்பதில் பிழை ஏற்பட்டது", - "bookmark-doesnt-exist": "புக்மார்க்கு இல்லை", - "user-doesnt-exist": "பயனர் இல்லை", - "reading-list-item-delete": "உருப்படி (களை) நீக்க முடியவில்லை", - "libraries-restricted": "பயனருக்கு எந்த நூலகங்களுக்கும் அணுகல் இல்லை", - "reading-list-title-required": "பட்டியல் தலைப்பு காலியாக இருக்க முடியாது", - "confirm-email": "முதலில் உங்கள் மின்னஞ்சலை உறுதிப்படுத்த வேண்டும்", - "locked-out": "பல அங்கீகார முயற்சிகளிலிருந்து நீங்கள் பூட்டப்பட்டுள்ளீர்கள். தயவுசெய்து 10 நிமிடங்கள் காத்திருக்கவும்.", - "disabled-account": "உங்கள் கணக்கு முடக்கப்பட்டுள்ளது. சேவையக நிர்வாகியைத் தொடர்பு கொள்ளுங்கள்.", - "register-user": "பயனரை பதிவு செய்யும் போது ஏதோ தவறு ஏற்பட்டது", - "validate-email": "உங்கள் மின்னஞ்சலை உறுதிப்படுத்தும் சிக்கல் இருந்தது: {0}", - "confirm-token-gen": "உறுதிப்படுத்தல் கிள்ளாக்கை உருவாக்கும் சிக்கல் இருந்தது", - "denied": "அனுமதிக்கப்படவில்லை", - "permission-denied": "இந்த நடவடிக்கைக்கு நீங்கள் அனுமதிக்கப்படவில்லை", - "password-required": "நீங்கள் ஒரு நிர்வாகி இல்லையென்றால் உங்கள் கணக்கை மாற்ற உங்கள் ஏற்கனவே உள்ள கடவுச்சொல்லை உள்ளிட வேண்டும்", - "invalid-password": "தவறான கடவுச்சொல்", - "invalid-token": "தவறான கிள்ளாக்கு", - "unable-to-reset-key": "விசையை மீட்டமைக்க முடியாமல் ஏதோ தவறு நடந்தது", - "invalid-payload": "தவறான பேலோட்", - "nothing-to-do": "செய்ய எதுவும் இல்லை", - "share-multiple-emails": "பல கணக்குகளில் மின்னஞ்சல்களைப் பகிர முடியாது", - "generate-token": "உறுதிப்படுத்தல் மின்னஞ்சல் கிள்ளாக்கை உருவாக்கும் சிக்கல் இருந்தது. பதிவுகள் பார்க்கவும்", - "no-user": "பயனர் இல்லை", - "username-taken": "ஏற்கனவே எடுக்கப்பட்ட பயனர்பெயர்", - "email-taken": "ஏற்கனவே பயன்பாட்டில் உள்ள மின்னஞ்சல்", - "user-already-confirmed": "பயனர் ஏற்கனவே உறுதிப்படுத்தப்பட்டுள்ளது", - "generic-user-update": "பயனரைப் புதுப்பிக்கும்போது விதிவிலக்கு இருந்தது", - "manual-setup-fail": "கையேடு அமைப்பை முடிக்க முடியவில்லை. தயவுசெய்து அழைப்பை ரத்து செய்து மீண்டும் உருவாக்கவும்", - "user-already-registered": "பயனர் ஏற்கனவே {0 அச் என பதிவு செய்யப்பட்டுள்ளார்", - "account-email-invalid": "நிர்வாகக் கணக்கிற்கான கோப்பில் உள்ள மின்னஞ்சல் சரியான மின்னஞ்சல் அல்ல. சோதனை மின்னஞ்சலை அனுப்ப முடியாது.", - "email-settings-invalid": "மின்னஞ்சல் அமைப்புகள் காணவில்லை. அனைத்து மின்னஞ்சல் அமைப்புகளும் சேமிக்கப்படுவதை உறுதிசெய்க.", - "chapter-doesnt-exist": "அத்தியாயம் இல்லை", - "file-missing": "கோப்பு புத்தகத்தில் காணப்படவில்லை", - "collection-updated": "சேகரிப்பு வெற்றிகரமாக புதுப்பிக்கப்பட்டது", - "collection-deleted": "சேகரிப்பு நீக்கப்பட்டது", - "generic-error": "ஏதோ தவறு நடந்தது, தயவுசெய்து மீண்டும் முயற்சிக்கவும்", - "collection-doesnt-exist": "சேகரிப்பு இல்லை", - "collection-already-exists": "சேகரிப்பு ஏற்கனவே உள்ளது", - "error-import-stack": "மால் ச்டேக்கை இறக்குமதி செய்வதில் சிக்கல் இருந்தது", - "person-doesnt-exist": "நபர் இல்லை", - "person-name-required": "நபரின் பெயர் தேவை மற்றும் பூச்யமாக இருக்கக்கூடாது", - "person-name-unique": "நபரின் பெயர் தனித்துவமாக இருக்க வேண்டும்", - "person-image-doesnt-exist": "கவர்ச்டிபியில் நபர் இல்லை", - "device-doesnt-exist": "சாதனம் இல்லை", - "generic-device-create": "சாதனத்தை உருவாக்கும் போது பிழை ஏற்பட்டது", - "generic-device-update": "சாதனத்தைப் புதுப்பிக்கும்போது பிழை ஏற்பட்டது", - "generic-device-delete": "சாதனத்தை நீக்கும்போது பிழை ஏற்பட்டது", - "greater-0": "{0} 0 ஐ விட அதிகமாக இருக்க வேண்டும்", - "send-to-kavita-email": "மின்னஞ்சல் அமைவு இல்லாமல் சாதனத்திற்கு அனுப்பு பயன்படுத்த முடியாது", - "send-to-unallowed": "உங்களுடையது இல்லாத சாதனத்திற்கு நீங்கள் அனுப்ப முடியாது", - "send-to-size-limit": "நீங்கள் அனுப்ப முயற்சிக்கும் கோப்பு (கள்) உங்கள் மின்னஞ்சல் வழங்குநருக்கு மிகப் பெரியது", - "send-to-device-status": "உங்கள் சாதனத்திற்கு கோப்புகளை மாற்றுகிறது", - "generic-send-to": "சாதனத்திற்கு கோப்பு (களை) அனுப்புவதில் பிழை ஏற்பட்டது", - "series-doesnt-exist": "தொடர் இல்லை", - "volume-doesnt-exist": "தொகுதி இல்லை", - "bookmarks-empty": "புக்மார்க்குகள் காலியாக இருக்க முடியாது", - "no-cover-image": "கவர் படம் இல்லை", - "delete-library-while-scan": "ச்கேன் நடந்து கொண்டிருக்கும்போது நீங்கள் ஒரு நூலகத்தை நீக்க முடியாது. கவிதாவை ச்கேன் முடிக்க அல்லது மறுதொடக்கம் செய்ய காத்திருங்கள்", - "generic-library-update": "நூலகத்தைப் புதுப்பிப்பதில் ஒரு முக்கியமான சிக்கல் இருந்தது.", - "pdf-doesnt-exist": "பி.டி.எஃப் எப்போது இருக்க வேண்டும்", - "invalid-access": "தவறான அணுகல்", - "no-image-for-page": "பக்கத்திற்கு அத்தகைய படம் இல்லை {0}. மறு கேச் அனுமதிக்க புத்துணர்ச்சியை முயற்சிக்கவும்.", - "perform-scan": "தயவுசெய்து இந்த தொடர் அல்லது நூலகத்தில் ச்கேன் செய்து மீண்டும் முயற்சிக்கவும்", - "generic-read-progress": "முன்னேற்றத்தை மிச்சப்படுத்தும் சிக்கல் இருந்தது", - "generic-clear-bookmarks": "புக்மார்க்குகளை அழிக்க முடியவில்லை", - "bookmark-permission": "புக்மார்க்கு/புத்தகமார்க்குக்கு உங்களுக்கு இசைவு இல்லை", - "bookmark-save": "புத்தகக்குறியை சேமிக்க முடியவில்லை", - "cache-file-find": "தற்காலிக சேமிப்பு படத்தைக் கண்டுபிடிக்க முடியவில்லை. மீண்டும் ஏற்றவும் மீண்டும் முயற்சிக்கவும்.", - "name-required": "பெயர் காலியாக இருக்க முடியாது", - "valid-number": "செல்லுபடியாகும் பக்க எண்ணாக இருக்க வேண்டும்", - "duplicate-bookmark": "நகல் புத்தகக்குறி நுழைவு ஏற்கனவே உள்ளது", - "reading-list-permission": "இந்த வாசிப்பு பட்டியலில் உங்களிடம் இசைவு இல்லை அல்லது பட்டியல் இல்லை", - "reading-list-position": "நிலையை புதுப்பிக்க முடியவில்லை", - "reading-list-updated": "புதுப்பிக்கப்பட்டது", - "reading-list-deleted": "வாசிப்பு பட்டியல் நீக்கப்பட்டது", - "generic-reading-list-delete": "வாசிப்பு பட்டியலை நீக்குவதில் சிக்கல் இருந்தது", - "generic-reading-list-update": "வாசிப்பு பட்டியலைப் புதுப்பிப்பதில் சிக்கல் இருந்தது", - "generic-reading-list-create": "வாசிப்பு பட்டியலை உருவாக்கும் சிக்கல் இருந்தது", - "reading-list-doesnt-exist": "வாசிப்பு பட்டியல் இல்லை", - "series-restricted": "பயனருக்கு இந்த தொடருக்கான அணுகல் இல்லை", - "generic-scrobble-hold": "பிடியைச் சேர்க்கும்போது பிழை ஏற்பட்டது", - "no-series": "நூலகத்திற்கான தொடர்களைப் பெற முடியவில்லை", - "encode-as-warning": "நீங்கள் பி.என்.சி.க்கு மாற்ற முடியாது. அட்டைகளுக்கு, புதுப்பிப்பு அட்டைகளைப் பயன்படுத்தவும். புக்மார்க்குகள் மற்றும் ஃபாவிகான்களை மீண்டும் குறியாக்கம் செய்ய முடியாது.", - "ip-address-invalid": "ஐபி முகவரி '{0}' தவறானது", - "bookmark-dir-permissions": "கவிதாவைப் பயன்படுத்த புக்மார்க்கு கோப்பகத்திற்கு சரியான அனுமதிகள் இல்லை", - "total-backups": "மொத்த காப்புப்பிரதிகள் 1 முதல் 30 வரை இருக்க வேண்டும்", - "total-logs": "மொத்த பதிவுகள் 1 முதல் 30 வரை இருக்க வேண்டும்", - "stats-permission-denied": "மற்றொரு பயனரின் புள்ளிவிவரங்களைக் காண உங்களுக்கு ஏற்பு இல்லை", - "url-not-valid": "முகவரி சரியான படத்தை திருப்பித் தராது அல்லது ஏற்பு தேவைப்படுகிறது", - "url-required": "பயன்படுத்த நீங்கள் ஒரு முகவரி ஐ அனுப்ப வேண்டும்", - "generic-cover-series-save": "கவர் படத்தை தொடருக்கு சேமிக்க முடியவில்லை", - "generic-cover-collection-save": "கவர் படத்தை சேகரிப்புக்கு சேமிக்க முடியவில்லை", - "generic-cover-reading-list-save": "கவர் படத்தை வாசிப்பு பட்டியலுக்கு சேமிக்க முடியவில்லை", - "generic-cover-chapter-save": "கவர் படத்தை அத்தியாயத்தில் சேமிக்க முடியவில்லை", - "generic-cover-library-save": "கவர் படத்தை நூலகத்தில் சேமிக்க முடியவில்லை", - "generic-cover-person-save": "கவர் படத்தை நபருக்கு சேமிக்க முடியவில்லை", - "generic-cover-volume-save": "கவர் படத்தை தொகுதிக்கு சேமிக்க முடியவில்லை", - "access-denied": "உங்களுக்கு அணுகல் இல்லை", - "reset-chapter-lock": "அத்தியாயத்திற்கான கவர் பூட்டை மீட்டமைக்க முடியவில்லை", - "generic-user-delete": "பயனரை நீக்க முடியவில்லை", - "generic-user-pref": "விருப்பங்களை சேமிக்கும் சிக்கல் இருந்தது", - "opds-disabled": "இந்த சேவையகத்தில் OPDS இயக்கப்படவில்லை", - "on-deck": "டெக்கில்", - "browse-on-deck": "டெக்கில் உலாவுக", - "recently-added": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது", - "want-to-read": "படிக்க விரும்புகிறேன்", - "browse-want-to-read": "உலாவு படிக்க விரும்புகிறது", - "browse-recently-added": "உலாவு அண்மைக் காலத்தில் சேர்க்கப்பட்டது", - "reading-lists": "பட்டியல்களைப் படித்தல்", - "sidenav-stream-only-delete-smart-filter": "சைடனாவிலிருந்து அறிவுள்ள வடிகட்டி நீரோடைகளை மட்டுமே நீக்க முடியும்", - "dashboard-stream-only-delete-smart-filter": "டாச்போர்டில் இருந்து அறிவுள்ள வடிகட்டி ச்ட்ரீம்களை மட்டுமே நீக்க முடியும்", - "smart-filter-name-required": "அறிவுள்ள வடிகட்டி பெயர் தேவை", - "smart-filter-system-name": "வழங்கப்பட்ட ச்ட்ரீமின் பெயரை நீங்கள் பயன்படுத்த முடியாது", - "kavitaplus-restricted": "இது கவிதா+ க்கு மட்டுமே", - "aliases-have-overlap": "ஒன்று அல்லது அதற்கு மேற்பட்ட மாற்றுப்பெயர்கள் மற்றவர்களுடன் ஒன்றுடன் ஒன்று உள்ளன, புதுப்பிக்க முடியாது", - "generated-reading-profile-name": "{0 இருந்து இலிருந்து உருவாக்கப்பட்டது" -} diff --git a/API/I18N/th.json b/API/I18N/th.json index 1988bcad9..9750001da 100644 --- a/API/I18N/th.json +++ b/API/I18N/th.json @@ -29,7 +29,7 @@ "generic-clear-bookmarks": "ไม่สามารถล้างบุ๊กมาร์ก", "cache-file-find": "ไม่พบรูปภาพที่เก็บไว้ โหลดใหม่และลองอีกครั้ง", "url-required": "คุณต้องส่ง url เพื่อใช้งาน", - "send-to-kavita-email": "ส่งไปยังอุปกรณ์ไม่สามารถใช้งานได้หากไม่มีการตั้งค่าอีเมล", + "send-to-kavita-email": "ส่งไปยังอุปกรณ์ใช้กับบริการอีเมลของ Kavita ไม่ได้ โปรดกำหนดค่าของคุณเอง", "favicon-doesnt-exist": "ไม่มีไอคอน Favicon", "library-name-exists": "ชื่อไลบรารีมีอยู่แล้ว โปรดเลือกชื่อเฉพาะสำหรับเซิร์ฟเวอร์", "library-doesnt-exist": "ไม่มีไลบรารี", @@ -78,6 +78,7 @@ "series-restricted-age-restriction": "ผู้ใช้ไม่ได้รับอนุญาตให้ดูซีรีส์นี้เนื่องจากการจำกัดอายุ", "issue-num": "ฉบับ {0}{1}", "confirm-email": "คุณต้องยืนยันอีเมลของคุณก่อน", + "bad-credentials": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง", "locked-out": "ไม่สามารถเข้าสู่ระบบได้เนื่องจากเข้าสู่ระบบล้มเหลวมากเกินไป กรุณารอ 10 นาที", "disabled-account": "บัญชีของคุณถูกระงับ กรุณาติดต่อผู้ดูแลระบบ", "denied": "ไม่อนุญาต", @@ -157,19 +158,5 @@ "epub-html-missing": "ไม่พบ html ที่เหมาะสมสำหรับหน้านั้น", "collection-tag-title-required": "ชื่อคอลเลกชันต้องไม่ว่างเปล่า", "want-to-read": "ต้องการอ่าน", - "browse-want-to-read": "ดูรายการต้องการอ่าน", - "email-not-enabled": "ไม่ได้เปิดใช้งานอีเมลบนเซิร์ฟเวอร์นี้ คุณไม่สามารถดำเนินการนี้ได้", - "send-to-unallowed": "คุณไม่สามารถส่งไปยังอุปกรณ์ที่ไม่ใช่ของคุณ", - "send-to-size-limit": "ไฟล์ที่คุณพยายามส่งมีขนาดใหญ่เกินไปสำหรับอีเมลของคุณ", - "browse-more-in-genre": "ค้นหาเพิ่มเติมใน {0}", - "recently-updated": "อัปเดตล่าสุด", - "more-in-genre": "เพิ่มเติมในประเภท {0}", - "browse-recently-updated": "เรียกดูอัปเดตล่าสุด", - "external-sources": "แหล่งข้อมูลภายนอก", - "browse-external-sources": "เรียกดูแหล่งข้อมูลภายนอก", - "smart-filters": "ตัวกรองอัจฉริยะ", - "browse-smart-filters": "เรียกดูตามตัวกรองอัจฉริยะ", - "smart-filter-doesnt-exist": "ไม่มีตัวกรองอัจฉริยะ", - "collection-deleted": "ลบคอลเล็กชั่นแล้ว", - "invalid-email": "อีเมลในไฟล์สำหรับผู้ใช้ไม่ใช่อีเมลที่ถูกต้อง ดูบันทึกสำหรับลิงก์ต่างๆ" + "browse-want-to-read": "ดูรายการต้องการอ่าน" } diff --git a/API/I18N/tr.json b/API/I18N/tr.json index 370f13f20..eb39e7192 100644 --- a/API/I18N/tr.json +++ b/API/I18N/tr.json @@ -1,9 +1,10 @@ { "denied": "İzin verilmedi", "permission-denied": "Bu operasyona izniniz yok", + "bad-credentials": "Kimlik bilgileriniz doğru değil", "confirm-email": "İlk olarak E-Posta'nı onaylaman gerek", "register-user": "Kullanıcıyı kayıt ederken bir şeyler yanlış gitti", - "disabled-account": "Hesabınız devre dışı. Sunucu yöneticisiyle iletişime geçin.", + "disabled-account": "Hesabınız devre dışı bırakıldı. Sunucu yöneticisiyle iletişime geçin.", "validate-email": "E-Posta'yı doğrularken bir hata oluştu: {0}", "confirm-token-gen": "Doğrulama tokeni oluşturulurken bir sorun oluştu", "password-required": "Yönetici değilseniz, hesabınızı değiştirmek için mevcut şifrenizi girmelisiniz", diff --git a/API/I18N/uk.json b/API/I18N/uk.json deleted file mode 100644 index 62f514a04..000000000 --- a/API/I18N/uk.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "confirm-email": "Ви маєте спершу підтвердити свій email", - "locked-out": "Забагато спроб авторизації. Доступ тимчасово неможливий. Почекайте 10 хвилин.", - "disabled-account": "Ваш акаунт заблоковано. Звʼяжіться з адміністратором сервера.", - "register-user": "Щось пішло не так під час реєстрації користувача", - "validate-email": "Сталася помилка під час підтвердження вашого email: {0}", - "denied": "Заборонено", - "permission-denied": "У вас нема дозволу для цієї операції", - "password-required": "Ви маєте ввести свій чинний пароль, щоб змінити обліковий запис (якщо тільки ви не адміністратор)", - "invalid-token": "Неправильний код", - "unable-to-reset-key": "Щось пішло не так – скинути ключ не вдалося", - "invalid-payload": "Некоректний зміст", - "nothing-to-do": "Тут нема роботи", - "share-multiple-emails": "Не можна користуватися одним email з кількох облікових записів", - "confirm-token-gen": "Щось пішло не так під час генерації коду підтвердження", - "invalid-password": "Неправильний пароль", - "generic-user-update": "Сталася помилка при спробі оновлення інформації користувача", - "generate-token": "Відбулась помилка при спробі підтвердження токену. Перегляньте логи", - "age-restriction-update": "Сталася помилка під час оновлення обмежень віку", - "no-user": "Користувача не існує", - "username-taken": "Ім'я користувача уже зайняте", - "user-already-confirmed": "Користувач уже підтверджений", - "email-taken": "Адреса електронної пошти уже зайнята" -} diff --git a/API/I18N/vi.json b/API/I18N/vi.json deleted file mode 100644 index 2fcd16154..000000000 --- a/API/I18N/vi.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "generic-user-email-update": "Không thể cập nhật email cho người dùng này. Vui lòng kiểm tra nhật ký.", - "forgot-password-generic": "Một email sẽ được gửi đến bạn nếu email đó tồn tại trong cơ sở dữ liệu của chúng tôi", - "not-accessible": "Máy chủ của bạn không thể được truy cập từ bên ngoài", - "generic-invite-email": "Đã xảy ra sự cố khi gửi lại email lời mời", - "admin-already-exists": "Tài khoản quản trị viên đã tồn tại", - "file-missing": "Không tìm thấy tập tin trong sách", - "generic-error": "Đã xảy ra lỗi. Vui lòng thử lại sau", - "device-doesnt-exist": "Thiết bị không tồn tại", - "send-to-kavita-email": "Không thể sử dụng tính năng chia sẻ tới thiết bị nếu không thiết lập chức năng Email", - "generic-device-create": "Đã xảy ra lỗi khi tạo thiết bị này", - "generic-invite-user": "Đã xảy ra sự cố khi mời người dùng này. Vui lòng kiểm tra nhật ký.", - "email-not-enabled": "Chức năng email không được kích hoạt trên máy chủ. Bạn không thể thực hiện hành động này.", - "register-user": "Đã xảy ra lỗi khi đăng ký tài khoản", - "confirm-token-gen": "Đã xảy ra sự cố khi tạo token xác minh", - "denied": "Không có quyền", - "permission-denied": "Bạn có quyền để thực hiện hành động này", - "invalid-password": "Mật khẩu không hợp lệ", - "invalid-token": "Token không hợp lệ", - "invalid-payload": "Payload không hợp lệ", - "username-taken": "Tên người dùng này đã được sử dụng", - "age-restriction-update": "Đã xảy ra lỗi khi cập nhật giới hạn độ tuổi", - "invalid-email-confirmation": "Email xác nhận không hợp lệ", - "user-already-registered": "Người dùng này đã được đăng ký với tên {0}", - "manual-setup-fail": "Không thể hoàn thành thiết lập thủ công. Vui lòng hủy và tạo lại lời mời", - "generic-password-update": "Đã xảy ra sự cố khi xác nhận mật khẩu mới", - "password-updated": "Mật khẩu đã được cập nhật", - "email-sent": "Đã gửi email", - "invalid-username": "Tên người dùng không hợp lệ", - "chapter-doesnt-exist": "Chương không tồn tại", - "collection-updated": "Bộ sưu tập đã cập nhật thành công", - "collection-deleted": "Đã xoá bộ sưu tập", - "collection-doesnt-exist": "Bộ sưu tập không tồn tại", - "generic-device-update": "Đã xảy ra lỗi khi cập nhật thông tin của thiết bị", - "generic-device-delete": "Đã xảy ra lỗi khi xóa thiết bị", - "greater-0": "Giá trị {0} phải lớn hơn 0", - "confirm-email": "Bạn cần phải xác minh email của mình trước", - "disabled-account": "Tài khoản của bạn đã bị vô hiệu hóa. Vui lòng liên hệ với quản trị viên.", - "validate-email": "Đã xảy ra sự cố khi xác thực email của bạn: {0}", - "password-required": "Bạn phải nhập mật khẩu hiện tại để đổi thông tin tài khoản của mình trừ khi bạn là quản trị viên", - "nothing-to-do": "Không có gì để thực hiện", - "no-user": "Người dùng không tồn tại", - "user-already-confirmed": "Người dùng này đã xác minh", - "generic-user-update": "Có sự cố đã xảy ra khi cập nhật thông tin người dùng", - "user-already-invited": "Người dùng đã được mời qua email này nhưng chưa chấp nhận lời mời.", - "generate-token": "Đã xảy ra sự cố khi tạo mã xác nhận email. Xem bản ghi", - "locked-out": "Bạn đã bị khóa do quá nhiều lần thử đăng nhập. Vui lòng chờ 10 phút.", - "unable-to-reset-key": "Có sự cố xảy ra, không thể đặt lại khóa", - "share-multiple-emails": "Một Email không thể được dùng chung cho nhiều tải khoản", - "file-doesnt-exist": "Tập tin không tồn tại", - "invalid-email": "Email được lưu cho người dùng không hợp lệ. Xem nhật ký để nhận link.", - "send-to-unallowed": "Bạn không thể gửi cho thiết bị của người khác", - "account-email-invalid": "Email được lưu cho tài khoản quản trị viên không hợp lệ. Không gửi được email thử.", - "not-accessible-password": "Máy chủ không truy cập được. Đường dẫn đặt lại mật khẩu nằm ở trong nhật ký", - "user-doesnt-exist": "Người dùng không tồn tại", - "library-doesnt-exist": "Thư viện không tồn tại", - "invalid-path": "Đường dẫn không hợp lệ", - "delete-library-while-scan": "Không thể xóa thư viện trong lúc đang quét kiểm tra sách. Xin đợi quá trình quét hoàn tất hoặc khởi động lại Kavita để xóa", - "collection-already-exists": "Bộ sưu tập đã tồn tại", - "send-to-size-limit": "Tệp tin bạn đang cố gắng gửi quá lớn đối với nhà cung cấp email của bạn", - "send-to-device-status": "Đang chuyển tập tin qua thiết bị", - "invalid-filename": "Tên tập tin không hợp lệ", - "library-name-exists": "Tên thư viện đã tồn tại. Xin chọn một tên có 1-0-2 trong nội bộ máy chủ này.", - "pdf-doesnt-exist": "PDF không tồn tại", - "critical-email-migration": "Có vấn đề trong việc chuyển email. Hãy liên hệ nhóm hỗ trợ", - "generic-send-to": "Đã xảy ra lỗi khi gửi tập tin đến thiết bị", - "email-settings-invalid": "Cài đặt email bị thiếu thông tin. Hãy đảm bảo tất cả cài đặt email đã được lưu lại.", - "must-be-defined": "Giá trị {0} phải được xác định", - "generic-favicon": "Đã xảy ra lỗi khi lấy favicon cho tên miền", - "no-library-access": "Người dùng không có quyền truy cập thư viện này", - "invalid-access": "Truy cập không hợp lệ", - "no-image-for-page": "Không có hình ảnh cho trang {0}. Thử làm mới trang để chạy cache lại.", - "bookmarks-empty": "Thẻ đánh dấu sách không được để trống", - "reading-list-doesnt-exist": "Danh sách đọc không tồn tại", - "reading-list-permission": "Bạn không có quyền truy cập danh sách đọc này hoặc danh sách không tồn tại", - "generic-library": "Có sự cố nghiêm trọng xảy ra. Xin thử lại sau.", - "generic-library-update": "Có sự cố nghiêm trọng xảy ra khi cập nhật thư viện.", - "bookmark-save": "Không thể lưu thẻ đánh dấu sách", - "cache-file-find": "Không thể tìm thấy hình ảnh được lưu trong bộ nhớ đệm. Tải trang và thử lại.", - "name-required": "Tên không được để trống", - "valid-number": "Phải là một số trang hợp lệ", - "duplicate-bookmark": "Trùng thẻ dánh dấu sách đã tồn tại", - "reading-list-position": "Không cập nhật được vị trí", - "reading-list-updated": "Đã cập nhật", - "reading-list-item-delete": "Không thể xóa các mục", - "reading-list-deleted": "Danh sách đọc đã bị xóa", - "generic-reading-list-delete": "Có lỗi xảy ra khi xóa danh sách đọc", - "generic-reading-list-update": "Có lỗi xảy ra khi cập nhật danh sách đọc", - "generic-reading-list-create": "Có lỗi xảy ra khi tạo danh sách đọc", - "libraries-restricted": "Người dùng không có quyền truy cập vào thư viện nào", - "generic-clear-bookmarks": "Không thể xóa thẻ đánh dấu sách", - "bookmark-permission": "Bạn không được cấp quyền đánh dấu/gỡ đánh dấu sách", - "no-cover-image": "Không ảnh bìa", - "bookmark-doesnt-exist": "Thẻ đánh dấu sách không tồn tại", - "generic-read-progress": "Có trục trặc xảy ra khi lưu tiến độ đọc", - "user-migration-needed": "Người dùng này cần di chuyển. Mời họ đăng xuất và đăng nhập lại để khởi động quá trình di chuyển", - "search": "Tìm kiếm", - "error-import-stack": "Có trục trặc khi nhập danh sách MAL", - "recently-added": "Mới thêm vào", - "update-metadata-fail": "Không thể cập nhật metadata", - "age-restriction-not-applicable": "Không giới hạn", - "generic-relationship": "Có vấn đề khi cập nhật mối quan hệ", - "job-already-running": "Tác vụ đang chạy", - "ip-address-invalid": "Địa chỉ IP '{0}' không hợp lệ", - "bookmark-dir-permissions": "Thư mục Dấu trang không được cấp đúng quyền cho Kavita sử dụng", - "total-backups": "Tổng số Sao lưu phải từ 1 đến 30", - "total-logs": "Tổng số Log phải từ 1 đến 30", - "generic-cover-reading-list-save": "Không thể lưu ảnh bìa vào Danh sách đọc", - "generic-cover-chapter-save": "Không thể lưu ảnh bìa vào Chương", - "reset-chapter-lock": "Không thể đặt lại khóa ảnh bìa cho Chương", - "device-not-created": "Thiết bị chưa tồn tại. Vui lòng tạo thiết bị trước", - "generic-cover-collection-save": "Không thể lưu ảnh bìa vào Bộ sưu tập", - "generic-cover-person-save": "Không thể lưu ảnh bìa vào Người dùng", - "device-duplicate": "Đã có một thiết bị với tên này", - "generic-cover-volume-save": "Không thể lưu ảnh bìa vào Tập", - "browse-on-deck": "Duyệt sách Đang đọc", - "want-to-read": "Muốn đọc", - "browse-recently-updated": "Lướt Mới cập nhật", - "browse-smart-filters": "Lướt theo Bộ lọc thông minh", - "search-description": "Tìm kiếm Bộ Truyện, Bộ sưu tập, hoặc Danh sách đọc", - "external-source-already-exists": "Đã có Nguồn bên ngoài này rồi", - "epub-html-missing": "Không tìm được html thích hợp cho trang", - "collection-tag-title-required": "Tiêu đề Bộ sưu tập không được để trống", - "reading-list-title-required": "Tiêu đề Danh sách đọc không được để trống", - "collection-tag-duplicate": "Trùng tên với bộ sưu tập hiện có", - "generic-cover-library-save": "Không thể lưu ảnh bìa vào Thư viện", - "access-denied": "Bạn không có quyền truy cập", - "generic-user-pref": "Có sự cố khi lưu tùy chọn", - "browse-reading-lists": "Lướt theo Danh sách đọc", - "browse-more-in-genre": "Lướt thêm trong {0}", - "reading-list-restricted": "Danh sách đọc không tồn tại hoặc bạn không có quyền truy cập", - "query-required": "Bạn phải truyền một tham số truy vấn", - "favicon-doesnt-exist": "Favicon không tồn tại", - "smart-filter-doesnt-exist": "Bộ lọc thông minh không tồn tại", - "external-source-doesnt-exist": "Nguồn bên ngoài không tồn tại", - "not-authenticated": "Người dùng không được cấp quyền", - "theme-doesnt-exist": "Tập tin hủ đề bị thiếu hoặc không hợp lệ", - "series-restricted-age-restriction": "Người dùng không được xem chuỗi vì giới hạn độ tuổi", - "chapter-num": "Chương {0}", - "reading-list-name-exists": "Trùng tên với một danh sách đọc hiện có", - "user-no-access-library-from-series": "Người dùng không có quyền truy cập vào thư viện chứa bộ truyện này", - "check-updates": "Kiểm tra cập nhật", - "scan-libraries": "Quét Thư viện", - "kavita+-data-refresh": "Tải lại dữ liệu Kavita+", - "series-updated": "Cập nhật thành công", - "stats-permission-denied": "Bạn không được phép xem số liệu thống kê của người dùng khác", - "url-not-valid": "Đường dẫn không hiển thị một hình ảnh hợp lệ hoặc yêu cầu xác minh", - "url-required": "Bạn cần nhập một đường dẫn để sử dụng", - "on-deck": "Đang đọc", - "browse-libraries": "Lướt theo Thư viện", - "collections": "Tất cả Bộ sưu tập", - "smart-filters": "Bộ lọc thông minh", - "report-stats": "Báo cáo Thống kê", - "cleanup": "Dọn sạch", - "backup": "Sao lưu", - "generic-user-delete": "Không thể xóa người dùng", - "opds-disabled": "OPDS chưa được bật trên máy chủ này", - "browse-want-to-read": "Duyệt Sách Muốn Đọc", - "browse-recently-added": "Duyệt Sách Mới thêm vào", - "reading-lists": "Danh sách đọc", - "libraries": "Tất cả Thư viện", - "browse-collections": "Lướt theo Bộ sưu tập", - "more-in-genre": "Xem thêm Thể loại {0}", - "recently-updated": "Mới cập nhật", - "external-sources": "Nguồn bên ngoài", - "browse-external-sources": "Lướt Nguồn bên ngoài", - "smart-filter-already-in-use": "Có một luồng đã tồn tại với Bộ lọc thông minh này", - "external-source-required": "Cần API Key và Host", - "unable-to-register-k+": "Không đăng ký bản quyền này được vì có lỗi. Hãy liên hệ Hỗ trợ Kavita+", - "unable-to-reset-k+": "Không đặt lại bản quyền Kavita+ được vì có lỗi. Hãy liên hệ Hỗ trợ Kavita+", - "anilist-cred-expired": "Thông tin xác thực AniList đã hết hạn hoặc chưa được thiết lập", - "epub-malformed": "Tập tin bị lỗi! Không đọc được.", - "send-to-permission": "Không thể gửi tệp tin FDF hoặc định dạng khác EPUB đến Kindle vì thiết bị này không hỗ trợ", - "volume-num": "Tập {0}", - "book-num": "Sách {0}", - "issue-num": "Kỳ {0}{1}", - "license-check": "Kiểm tra bản quyền", - "remove-from-want-to-read": "Dọn sạch Muốn đọc", - "update-yearly-stats": "Cập nhật Thống kê hằng năm", - "generic-cover-series-save": "Không thể lưu ảnh bìa vào Series", - "external-source-already-in-use": "Có một luồng hiện có với Nguồn bên ngoài này", - "generic-scrobble-hold": "Đã xảy ra lỗi khi thêm lệnh giữ", - "generic-series-delete": "Đã có sự cố khi xóa Series này", - "encode-as-warning": "Bạn không thể chuyển đổi sang PNG. Đối với bìa, hãy sử dụng \"Làm mới Trang Bìa\". Không thể mã hóa lại dấu trang và biểu tượng yêu thích.", - "person-doesnt-exist": "Người dùng không tồn tại", - "series-restricted": "Người dùng không có quyền truy cập vào Series này", - "no-series": "Không thể lấy được series cho Thư viện", - "bad-copy-files-for-download": "Không thể sao chép tệp vào thư mục lưu trữ tạm thời.", - "process-processed-scrobbling-events": "Xử lý sự kiện Scrobbling đã xử lý", - "generic-series-update": "Đã xảy ra lỗi khi cập nhật series", - "dashboard-stream-doesnt-exist": "Bảng điều khiển Stream không tồn tại", - "sidenav-stream-doesnt-exist": "SideNav Stream không tồn tại", - "scrobble-bad-payload": "Bad payload từ Nhà cung cấp Scrobble", - "progress-must-exist": "Tiến trình phải tồn tại trên người dùng", - "generic-create-temp-archive": "Đã xảy ra sự cố khi tạo kho lưu trữ tạm thời", - "process-scrobbling-events": "Xử lý Sự kiện Scrobbling", - "check-scrobbling-tokens": "Kiểm tra Scrobbling Tokens", - "series-doesnt-exist": "Series không tồn tại", - "volume-doesnt-exist": "Tập không tồn tại", - "perform-scan": "Vui lòng thực hiện quét trên Series này hoặc thư viện và thử lại", - "no-series-collection": "Không thể lấy được series cho Collection", - "person-name-required": "Tên người là bắt buộc và không được để trống", - "person-name-unique": "Tên người phải là duy nhất", - "person-image-doesnt-exist": "Người không tồn tại trong CoversDB" -} diff --git a/API/I18N/zh_Hans.json b/API/I18N/zh_Hans.json index 14c8c902e..6a3dfb12d 100644 --- a/API/I18N/zh_Hans.json +++ b/API/I18N/zh_Hans.json @@ -1,4 +1,5 @@ { + "bad-credentials": "您的登录信息不正确", "validate-email": "验证你的邮件时出了点问题: {0}", "confirm-token-gen": "生成认证令牌时出现问题", "denied": "未被允许", @@ -8,7 +9,7 @@ "confirm-email": "您必须先确认电子邮件", "disabled-account": "您的账号已被禁用,请联系管理员。", "register-user": "注册用户时出现一些错误", - "locked-out": "您因多次错误登陆已被阻止,请稍等 10 分钟。", + "locked-out": "您因多次错误登陆已被阻止,请稍等 10 分钟", "permission-denied": "您无权执行此操作", "invalid-password": "无效密码", "generate-token": "生成确认电子邮件令牌时出现问题,参见日志", @@ -105,7 +106,7 @@ "search-description": "搜索系列、收藏或阅读清单", "favicon-doesnt-exist": "图标不存在", "not-authenticated": "用户未通过身份验证", - "anilist-cred-expired": "AniList 凭据已过期或未设置", + "anilist-cred-expired": "AniList凭据已过期或未设置", "theme-doesnt-exist": "主题文件丢失或者无效", "generic-user-email-update": "无法更新用户的电子邮件。请检查日志。", "epub-malformed": "文件格式不正确!无法读取。", @@ -116,7 +117,7 @@ "collection-tag-title-required": "收藏标题不能为空", "reading-list-title-required": "阅读清单标题不能为空", "collection-tag-duplicate": "收藏的名称已存在", - "chapter-num": "{0}话", + "chapter-num": "第{0}话", "not-accessible-password": "您的服务器无法访问,重置密码的链接位于日志中", "invalid-payload": "无效的数据", "nothing-to-do": "没有需要处理的任务", @@ -125,7 +126,7 @@ "user-already-registered": "用户已经注册为{0}", "critical-email-migration": "更改电子邮件地址时出现问题,请联系支持人员", "chapter-doesnt-exist": "章节不存在", - "send-to-kavita-email": "如果没有电子邮件设置,则无法使用“发送到设备”", + "send-to-kavita-email": "无法使用Kavita服务向设备发送电子邮件,请自行配置。", "generic-favicon": "获取图标时出现问题", "pdf-doesnt-exist": "PDF文件应该存在,但未找到", "no-series": "无法获取资料库中的系列", @@ -142,9 +143,9 @@ "device-not-created": "设备不存在,请先创建", "send-to-permission": "无法向设备发送Kindel不支持的非EPUB格式或者PDF格式文件", "reading-list-name-exists": "此名称的阅读清单已存在", - "volume-num": "{0}卷", - "issue-num": "{0}{1}期号", - "book-num": "{0}本", + "volume-num": "第{0}卷", + "issue-num": "问题编号{0}{1}", + "book-num": "第{0}本", "user-migration-needed": "该用户需要进行迁移。通知他们注销并重新登录,以触发迁移流程", "generic-relationship": "更新关系时发生了问题", "encode-as-warning": "无法转换为PNG格式。对于封面,请使用刷新封面功能。书签和网站图标无法再进行编码。", @@ -153,8 +154,8 @@ "user-no-access-library-from-series": "用户无法访问此系列所属的资料库", "generic-create-temp-archive": "创建临时档案时出现问题", "query-required": "您必须传递一个查询参数", - "scrobble-bad-payload": "Scrobble 服务提供商的数据无效", - "bad-copy-files-for-download": "无法复制文件至临时下载目录。", + "scrobble-bad-payload": "Scrobble服务提供商的数据无效", + "bad-copy-files-for-download": "无法复制文件至临时下载目录", "progress-must-exist": "用户进程必须存在", "generic-scrobble-hold": "启用锁定时发生错误", "reset-chapter-lock": "无法重置章节的封面锁", @@ -170,44 +171,5 @@ "browse-external-sources": "浏览外部资源", "external-source-already-in-use": "存在具有此外部源的现有流", "external-sources": "外部来源", - "smart-filter-already-in-use": "存在带有此智能筛选器的现有流", - "invalid-email": "用户文件中的电子邮件不是有效的电子邮件。请参阅日志中的链接。", - "browse-more-in-genre": "浏览 {0} 中的更多内容", - "more-in-genre": "更多类型 {0}", - "recently-updated": "最近更新", - "browse-recently-updated": "浏览最近更新", - "unable-to-reset-k+": "因为一些错误导致无法重置 Kavita+ 许可证。请联系 Kavita+ 支持人员", - "email-not-enabled": "此服务器上未启用电子邮件。您无法执行此操作。", - "send-to-unallowed": "您无法发送到不属于您的设备", - "send-to-size-limit": "您尝试发送的文件对于您的电子邮件提供商来说太大", - "process-scrobbling-events": "处理 Scrobbling 事件", - "report-stats": "报告统计", - "check-updates": "检查更新", - "license-check": "许可证检查", - "cleanup": "清理", - "process-processed-scrobbling-events": "处理已处理的 Scrobbling 事件", - "check-scrobbling-tokens": "检查 Scrobbling Tokens", - "remove-from-want-to-read": "想读清理", - "scan-libraries": "扫描资料库", - "kavita+-data-refresh": "Kavita+ 数据刷新", - "backup": "备份", - "update-yearly-stats": "更新年度统计数据", - "email-settings-invalid": "电子邮件设置缺少信息。确保保存所有电子邮件设置。", - "account-email-invalid": "管理员帐户存档的电子邮件不是有效的电子邮件。无法发送测试电子邮件。", - "collection-already-exists": "收藏已存在", - "error-import-stack": "导入 MAL stack 时出现问题", - "generic-cover-volume-save": "无法将封面图片保存至卷", - "generic-cover-person-save": "无法将封面图片保存到人物", - "person-doesnt-exist": "人员不存在", - "person-name-required": "人员姓名为必填项,且不能为空", - "person-name-unique": "人名必须是唯一的", - "person-image-doesnt-exist": "CoversDB 中不存在此人", - "email-taken": "电子邮件已被使用", - "kavitaplus-restricted": "仅限 Kavita+", - "dashboard-stream-only-delete-smart-filter": "只能从仪表板中删除智能筛选器流", - "smart-filter-name-required": "需要智能筛选器名称", - "smart-filter-system-name": "您不能使用系统提供的流名称", - "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流", - "aliases-have-overlap": "一个或多个别名与其他人有重叠,无法更新", - "generated-reading-profile-name": "由 {0} 生成" + "smart-filter-already-in-use": "存在带有此智能筛选器的现有流" } diff --git a/API/I18N/zh_Hant.json b/API/I18N/zh_Hant.json index 31d4b69f6..04cf77530 100644 --- a/API/I18N/zh_Hant.json +++ b/API/I18N/zh_Hant.json @@ -17,7 +17,7 @@ "file-doesnt-exist": "檔案不存在", "admin-already-exists": "管理員已存在", "age-restriction-update": "更新年齡限制時發生錯誤", - "send-to-kavita-email": "未設置電子郵件時無法使用傳送到裝置功能", + "send-to-kavita-email": "無法將 Kavita 的電子郵件服務用於傳送到裝置。請設定您自己的電子郵件服務。", "not-accessible": "您的伺服器無法從外部存取", "collections": "所有收藏", "email-sent": "電子郵件已傳送", @@ -41,6 +41,7 @@ "generic-create-temp-archive": "建立暫存檔案時遇到問題", "bookmark-save": "無法儲存書籤", "bookmark-dir-permissions": "書籤目錄沒有 Kavita 可以使用的正確權限", + "bad-credentials": "您的帳號或密碼不正確", "total-backups": "總備份數量必須在 1 到 30 之間", "book-num": "書本 {0}", "generic-cover-series-save": "無法將封面圖片儲存到系列作品", @@ -161,53 +162,5 @@ "collection-deleted": "收藏已刪除", "permission-denied": "您不被允許進行此操作", "device-doesnt-exist": "裝置不存在", - "generic-series-delete": "刪除系列作品時發生問題", - "recently-updated": "最近更新", - "external-source-already-in-use": "已存在具有此外部來源的串流", - "report-stats": "統計報告", - "backup": "備份", - "more-in-genre": "更多關於類型 {0}", - "account-email-invalid": "管理員帳號檔案中的電子郵件無效。無法發送測試電子郵件。", - "email-not-enabled": "此伺服器未啟用電子郵件功能。您無法執行此操作。", - "error-import-stack": "匯入 MAL 時出現問題", - "send-to-unallowed": "您無法傳送到不是您自己的裝置", - "email-settings-invalid": "電子郵件設定缺少資訊。請確保所有電子郵件設定已保存。", - "collection-already-exists": "組合已存在", - "send-to-size-limit": "您嘗試傳送的文件過大,無法通過您的電子郵件服務提供商發送", - "external-sources": "外部來源", - "dashboard-stream-doesnt-exist": "儀表板串流不存在", - "unable-to-reset-k+": "發生錯誤,無法重置 Kavita+ 授權。請聯繫 Kavita+ 支援", - "check-scrobbling-tokens": "檢查 Scrobbling Tokens", - "cleanup": "清理", - "browse-more-in-genre": "在 {0} 中繼續瀏覽", - "browse-recently-updated": "瀏覽最近更新", - "external-source-required": "需要 API 金鑰和 Host", - "external-source-doesnt-exist": "外部來源不存在", - "check-updates": "檢查更新", - "license-check": "授權檢查", - "process-scrobbling-events": "處理 Scrobbling 事件", - "process-processed-scrobbling-events": "處理已處理的 Scrobbling 事件", - "remove-from-want-to-read": "清理閱讀清單", - "scan-libraries": "掃描資料庫", - "kavita+-data-refresh": "Kavita+ 資料更新", - "update-yearly-stats": "更新年度統計", - "invalid-email": "使用者檔案中的電子郵件無效。請查看日誌以獲得任何連結。", - "browse-external-sources": "瀏覽外部來源", - "sidenav-stream-doesnt-exist": "側邊導覽列的串流不存在", - "smart-filter-already-in-use": "已存在具有此智慧篩選器的串流", - "external-source-already-exists": "外部來源已存在", - "generic-cover-volume-save": "無法保存封面圖片", - "generic-cover-person-save": "無法保存封面圖片", - "person-doesnt-exist": "此人不存在", - "person-name-required": "名稱為必填欄位,且不得留空", - "person-name-unique": "名稱不得重複", - "person-image-doesnt-exist": "CoversDB 中不存在此人", - "email-taken": "電子郵件已被使用", - "sidenav-stream-only-delete-smart-filter": "只有智慧篩選器可以從側邊導覽列中刪除", - "dashboard-stream-only-delete-smart-filter": "只有智慧篩選器串流可以從儀表板中刪除", - "smart-filter-name-required": "智慧篩選器名稱名稱不可為空", - "smart-filter-system-name": "您不能使用系統保留的串流名稱", - "kavitaplus-restricted": "此功能僅限 Kavita+ 使用", - "aliases-have-overlap": "一個或多個別名與其他人物重複,無法更新", - "generated-reading-profile-name": "由 {0} 生成" + "generic-series-delete": "刪除系列作品時發生問題" } diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index 958792c84..1b4a2d9a8 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using Serilog; +using Serilog; using Serilog.Core; using Serilog.Events; using Serilog.Formatting.Display; @@ -50,7 +49,6 @@ public static class LogLevelOptions .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error) .Enrich.FromLogContext() .Enrich.WithThreadId() - .Enrich.With(new ApiKeyEnricher()) .WriteTo.Console(new MessageTemplateTextFormatter(outputTemplate)) .WriteTo.File(LogFile, shared: true, @@ -65,20 +63,17 @@ public static class LogLevelOptions e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) == "Serilog.AspNetCore.RequestLoggingMiddleware"; - // If Minimum log level is Warning, swallow all Request Logging messages - if (isRequestLoggingMiddleware && LogLevelSwitch.MinimumLevel > LogEventLevel.Information) + // If Minimum log level is Information, swallow all Request Logging messages + if (isRequestLoggingMiddleware && LogLevelSwitch.MinimumLevel >= LogEventLevel.Information) { return false; } if (isRequestLoggingMiddleware) { - var path = e.Properties["Path"].ToString().Replace("\"", string.Empty); - if (e.Properties.ContainsKey("Path") && path == "/api/health") return false; - if (e.Properties.ContainsKey("Path") && path == "/hubs/messages") return false; - if (e.Properties.ContainsKey("Path") && path.StartsWith("/api/image")) return false; + if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false; + if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false; } - return true; } @@ -120,24 +115,3 @@ public static class LogLevelOptions } } - -public partial class ApiKeyEnricher : ILogEventEnricher -{ - public void Enrich(LogEvent e, ILogEventPropertyFactory propertyFactory) - { - var isRequestLoggingMiddleware = e.Properties.ContainsKey("SourceContext") && - e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) == - "Serilog.AspNetCore.RequestLoggingMiddleware"; - if (!isRequestLoggingMiddleware) return; - if (!e.Properties.ContainsKey("RequestPath") || - !e.Properties["RequestPath"].ToString().Contains("apiKey=")) return; - - // Check if the log message contains "apiKey=" and censor it - var censoredMessage = MyRegex().Replace(e.Properties["RequestPath"].ToString(), "apiKey=******REDACTED******"); - var enrichedProperty = propertyFactory.CreateProperty("RequestPath", censoredMessage); - e.AddOrUpdateProperty(enrichedProperty); - } - - [GeneratedRegex(@"\bapiKey=[^&\s]+\b")] - private static partial Regex MyRegex(); -} diff --git a/API/Middleware/ExceptionMiddleware.cs b/API/Middleware/ExceptionMiddleware.cs index 0b2b308c9..98c3c6aec 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/API/Middleware/ExceptionMiddleware.cs @@ -9,17 +9,27 @@ using Microsoft.Extensions.Logging; namespace API.Middleware; -public class ExceptionMiddleware(RequestDelegate next, ILogger logger) +public class ExceptionMiddleware { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + + public ExceptionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + public async Task InvokeAsync(HttpContext context) { try { - await next(context); // downstream middlewares or http call + await _next(context); // downstream middlewares or http call } catch (Exception ex) { - logger.LogError(ex, "There was an exception"); + _logger.LogError(ex, "There was an exception"); context.Response.ContentType = "application/json"; context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; diff --git a/API/Middleware/JWTRevocationMiddleware.cs b/API/Middleware/JWTRevocationMiddleware.cs index 65ea9e80f..2bcba3425 100644 --- a/API/Middleware/JWTRevocationMiddleware.cs +++ b/API/Middleware/JWTRevocationMiddleware.cs @@ -10,16 +10,24 @@ namespace API.Middleware; /// /// Responsible for maintaining an in-memory. Not in use /// -public class JwtRevocationMiddleware( - RequestDelegate next, - IEasyCachingProviderFactory cacheFactory, - ILogger logger) +public class JwtRevocationMiddleware { + private readonly RequestDelegate _next; + private readonly IEasyCachingProviderFactory _cacheFactory; + private readonly ILogger _logger; + + public JwtRevocationMiddleware(RequestDelegate next, IEasyCachingProviderFactory cacheFactory, ILogger logger) + { + _next = next; + _cacheFactory = cacheFactory; + _logger = logger; + } + public async Task InvokeAsync(HttpContext context) { if (context.User.Identity is {IsAuthenticated: false}) { - await next(context); + await _next(context); return; } @@ -29,18 +37,18 @@ public class JwtRevocationMiddleware( // Check if the token is revoked if (await IsTokenRevoked(token)) { - logger.LogWarning("Revoked token detected: {Token}", token); + _logger.LogWarning("Revoked token detected: {Token}", token); context.Response.StatusCode = StatusCodes.Status401Unauthorized; return; } - await next(context); + await _next(context); } private async Task IsTokenRevoked(string token) { // Check if the token exists in the revocation list stored in the cache - var isRevoked = await cacheFactory.GetCachingProvider(EasyCacheProfiles.RevokedJwt) + var isRevoked = await _cacheFactory.GetCachingProvider(EasyCacheProfiles.RevokedJwt) .GetAsync(token); diff --git a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs b/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs index c2119bb13..f7732b7bd 100644 --- a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs +++ b/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; namespace API.Middleware.RateLimit; -#nullable enable public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy { diff --git a/API/Middleware/SecurityMiddleware.cs b/API/Middleware/SecurityMiddleware.cs index 67cb42d0c..5b2019594 100644 --- a/API/Middleware/SecurityMiddleware.cs +++ b/API/Middleware/SecurityMiddleware.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Net; using System.Text.Json; using System.Threading.Tasks; @@ -8,29 +7,37 @@ using API.Errors; using Kavita.Common; using Microsoft.AspNetCore.Http; using Serilog; -using ILogger = Serilog.Core.Logger; +using ILogger = Serilog.ILogger; namespace API.Middleware; -public class SecurityEventMiddleware(RequestDelegate next) +public class SecurityEventMiddleware { - private readonly ILogger _logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.File(Path.Join(Directory.GetCurrentDirectory(), "config/logs/", "security.log"), rollingInterval: RollingInterval.Day) - .CreateLogger(); + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public SecurityEventMiddleware(RequestDelegate next) + { + _next = next; + + _logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.File(Path.Join(Directory.GetCurrentDirectory(), "config/logs/", "security.log"), rollingInterval: RollingInterval.Day) + .CreateLogger(); + } public async Task InvokeAsync(HttpContext context) { try { - await next(context); + await _next(context); } catch (KavitaUnauthenticatedUserException ex) { - var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? context.Connection.RemoteIpAddress?.ToString(); + var ipAddress = context.Connection.RemoteIpAddress?.ToString(); var requestMethod = context.Request.Method; var requestPath = context.Request.Path; - var userAgent = context.Request.Headers.UserAgent; + var userAgent = context.Request.Headers["User-Agent"]; var securityEvent = new { IpAddress = ipAddress, @@ -50,7 +57,8 @@ public class SecurityEventMiddleware(RequestDelegate next) var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = + JsonNamingPolicy.CamelCase }; var json = JsonSerializer.Serialize(response, options); diff --git a/API/Program.cs b/API/Program.cs index 011a7de2a..09c1707af 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; @@ -21,14 +20,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using NetVips; using Serilog; using Serilog.Events; using Serilog.Sinks.AspNetCore.SignalR.Extensions; -using Log = Serilog.Log; namespace API; -#nullable enable public class Program { @@ -49,13 +45,18 @@ public class Program var directoryService = new DirectoryService(null!, new FileSystem()); - - // Check if this is the first time running and if so, rename appsettings-init.json to appsettings.json - HandleFirstRunConfiguration(); - - // Before anything, check if JWT has been generated properly or if user still has default - EnsureJwtTokenKey(); + if (!Configuration.CheckIfJwtTokenSet() && + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) + { + Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); + var rBytes = new byte[256]; + RandomNumberGenerator.Create().GetBytes(rBytes); + Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); + } + + Configuration.KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development + ? "http://localhost:5020" : "https://plus.kavitareader.com"; try { @@ -69,7 +70,6 @@ public class Program { var logger = services.GetRequiredService>(); var context = services.GetRequiredService(); - var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); var isDbCreated = await context.Database.CanConnectAsync(); if (isDbCreated && pendingMigrations.Any()) @@ -86,38 +86,6 @@ public class Program } } - // Apply Before manual migrations that need to run before actual migrations - if (isDbCreated) - { - Task.Run(async () => - { - // Apply all migrations on startup - logger.LogInformation("Running Manual Migrations"); - - try - { - // v0.7.14 - await MigrateWantToReadExport.Migrate(context, directoryService, logger); - - // v0.8.2 - await ManualMigrateSwitchToWal.Migrate(context, logger); - - // v0.8.4 - await ManualMigrateEncodeSettings.Migrate(context, logger); - } - catch (Exception ex) - { - /* Swallow */ - } - - await unitOfWork.CommitAsync(); - logger.LogInformation("Running Manual Migrations - complete"); - }).GetAwaiter() - .GetResult(); - } - - - await context.Database.MigrateAsync(); @@ -127,7 +95,6 @@ public class Program await Seed.SeedDefaultStreams(unitOfWork); await Seed.SeedDefaultSideNavStreams(unitOfWork); await Seed.SeedUserApiKeys(context); - await Seed.SeedMetadataSettings(context); } catch (Exception ex) { @@ -145,8 +112,6 @@ public class Program var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); LogLevelOptions.SwitchLogLevel(settings.LoggingLevel); - InitNetVips(); - await host.RunAsync(); } catch (Exception ex) { @@ -157,26 +122,6 @@ public class Program } } - private static void EnsureJwtTokenKey() - { - if (Configuration.CheckIfJwtTokenSet() || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) return; - - Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); - var rBytes = new byte[256]; - RandomNumberGenerator.Create().GetBytes(rBytes); - Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); - } - - private static void HandleFirstRunConfiguration() - { - var firstRunConfigFilePath = Path.Join(Directory.GetCurrentDirectory(), "config/appsettings-init.json"); - if (File.Exists(firstRunConfigFilePath) && - !File.Exists(Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json"))) - { - File.Move(firstRunConfigFilePath, Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json")); - } - } - private static async Task GetMigrationDirectory(DataContext context, IDirectoryService directoryService) { string? currentVersion = null; @@ -249,14 +194,4 @@ public class Program webBuilder.UseStartup(); }); - - /// - /// Ensure NetVips does not cache - /// - /// https://github.com/kleisauke/net-vips/issues/6#issuecomment-394379299 - private static void InitNetVips() - { - Cache.MaxFiles = 0; - - } } diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 74b6709fa..8e1ede54d 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -5,10 +5,8 @@ using System.Threading.Tasks; using System.Web; using API.Constants; using API.Data; -using API.DTOs.Account; using API.Entities; using API.Errors; -using API.Extensions; using Kavita.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -18,8 +16,6 @@ using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable - public interface IAccountService { Task> ChangeUserPassword(AppUser user, string newPassword); @@ -28,7 +24,9 @@ public interface IAccountService Task> ValidateEmail(string email); Task HasBookmarkPermission(AppUser? user); Task HasDownloadPermission(AppUser? user); - Task CanChangeAgeRestriction(AppUser? user); + Task HasChangeRestrictionRole(AppUser? user); + Task CheckIfAccessible(HttpRequest request); + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); } public class AccountService : IAccountService @@ -36,19 +34,54 @@ public class AccountService : IAccountService private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; + private readonly IHostEnvironment _environment; + private readonly IEmailService _emailService; public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; + private const string LocalHost = "localhost:4200"; - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork) + public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, + IHostEnvironment environment, IEmailService emailService) { _userManager = userManager; _logger = logger; _unitOfWork = unitOfWork; + _environment = environment; + _emailService = emailService; + } + + /// + /// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name. + /// + /// + /// + public async Task CheckIfAccessible(HttpRequest request) + { + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host); + } + + public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) + { + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + var basePart = $"{request.Scheme}://{host}{request.PathBase}/"; + if (!string.IsNullOrEmpty(serverSettings.HostName)) + { + basePart = serverSettings.HostName; + if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) + { + basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - 1); + } + } + + if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); + return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}".Replace("//", "/"); } public async Task> ChangeUserPassword(AppUser user, string newPassword) { var passwordValidationIssues = (await ValidatePassword(user, newPassword)).ToList(); - if (passwordValidationIssues.Count != 0) return passwordValidationIssues; + if (passwordValidationIssues.Any()) return passwordValidationIssues; var result = await _userManager.RemovePasswordAsync(user); if (!result.Succeeded) @@ -57,11 +90,15 @@ public class AccountService : IAccountService return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); } - result = await _userManager.AddPasswordAsync(user, newPassword); - if (result.Succeeded) return []; - _logger.LogError("Could not update password"); - return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); + result = await _userManager.AddPasswordAsync(user, newPassword); + if (!result.Succeeded) + { + _logger.LogError("Could not update password"); + return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); + } + + return new List(); } public async Task> ValidatePassword(AppUser user, string password) @@ -79,28 +116,26 @@ public class AccountService : IAccountService } public async Task> ValidateUsername(string username) { - // Reverted because of https://go.microsoft.com/fwlink/?linkid=2129535 - if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName != null - && x.NormalizedUserName == username.ToUpper())) + if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper())) { - return - [ - new(400, "Username is already taken") - ]; + return new List() + { + new ApiException(400, "Username is already taken") + }; } - return []; + return Array.Empty(); } public async Task> ValidateEmail(string email) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); - if (user == null) return []; + if (user == null) return Array.Empty(); - return - [ + return new List() + { new ApiException(400, "Email is already registered") - ]; + }; } /// @@ -112,7 +147,6 @@ public class AccountService : IAccountService { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); - return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole); } @@ -125,22 +159,19 @@ public class AccountService : IAccountService { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); - return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); } /// - /// Does the user have Change Restriction permission or admin rights and not Read Only + /// Does the user have Change Restriction permission or admin rights /// /// /// - public async Task CanChangeAgeRestriction(AppUser? user) + public async Task HasChangeRestrictionRole(AppUser? user) { if (user == null) return false; - var roles = await _userManager.GetRolesAsync(user); - if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false; - return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole); } + } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 335a5a74b..fd4349c90 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.Serialization; using API.Archive; @@ -16,12 +15,9 @@ using Kavita.Common; using Microsoft.Extensions.Logging; using SharpCompress.Archives; using SharpCompress.Common; -using YamlDotNet.Core; namespace API.Services; -#nullable enable - public interface IArchiveService { void ExtractArchive(string archivePath, string extractPath); @@ -32,21 +28,13 @@ public interface IArchiveService ArchiveLibrary CanOpen(string archivePath); bool ArchiveNeedsFlattening(ZipArchive archive); /// - /// Creates a zip file form the listed files and outputs to the temp folder. This will combine into one zip of multiple zips. + /// Creates a zip file form the listed files and outputs to the temp folder. /// /// List of files to be zipped up. Should be full file paths. /// Temp folder name to use for preparing the files. Will be created and deleted /// Path to the temp zip /// string CreateZipForDownload(IEnumerable files, string tempFolder); - /// - /// Creates a zip file form the listed files and outputs to the temp folder. This will extract each archive and combine them into one zip. - /// - /// List of files to be zipped up. Should be full file paths. - /// Temp folder name to use for preparing the files. Will be created and deleted - /// Path to the temp zip - /// - string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback); } /// @@ -128,11 +116,9 @@ public class ArchiveService : IArchiveService } case ArchiveLibrary.NotSupported: _logger.LogWarning("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; default: _logger.LogWarning("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; } } @@ -221,7 +207,7 @@ public class ArchiveService : IArchiveService /// public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default) { - if (string.IsNullOrEmpty(archivePath) || !IsValidArchive(archivePath)) return string.Empty; + if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty; try { var libraryHandler = CanOpen(archivePath); @@ -334,68 +320,6 @@ public class ArchiveService : IArchiveService return zipPath; } - public string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback) - { - var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); - - var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz")); - if (potentialExistingFile.Exists) - { - // A previous download exists, just return it immediately - return potentialExistingFile.FullName; - } - - // Extract all the files to a temp directory and create zip on that - var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); - var totalFiles = files.Count + 1; - var count = 1f; - try - { - _directoryService.ExistOrCreate(tempLocation); - foreach (var path in files) - { - var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name)); - - // Image series need different handling - if (Tasks.Scanner.Parser.Parser.IsImage(path)) - { - var parentDirectory = _directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name; - tempPath = Path.Join(tempLocation, parentDirectory ?? _directoryService.FileSystem.FileInfo.New(path).Name); - } - - if (Tasks.Scanner.Parser.Parser.IsArchive(path)) - { - // Archives don't need to be put into a subdirectory of the same name - tempPath = _directoryService.GetParentDirectoryName(tempPath); - } - - progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); - - _directoryService.CopyFileToDirectory(path, tempPath); - count++; - } - } - catch - { - throw new KavitaException("bad-copy-files-for-download"); - } - - var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz"); - try - { - ZipFile.CreateFromDirectory(tempLocation, zipPath); - // Remove the folder as we have the zip - _directoryService.ClearAndDeleteDirectory(tempLocation); - } - catch (AggregateException ex) - { - _logger.LogError(ex, "There was an issue creating temp archive"); - throw new KavitaException("generic-create-temp-archive"); - } - - return zipPath; - } - /// /// Test if the archive path exists and an archive @@ -410,7 +334,7 @@ public class ArchiveService : IArchiveService return false; } - if (Tasks.Scanner.Parser.Parser.IsArchive(archivePath)) return true; + if (Tasks.Scanner.Parser.Parser.IsArchive(archivePath) || Tasks.Scanner.Parser.Parser.IsEpub(archivePath)) return true; _logger.LogWarning("Archive {ArchivePath} is not a valid archive", archivePath); return false; @@ -551,7 +475,7 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) return; - if (_directoryService.FileSystem.Directory.Exists(extractPath)) return; + if (Directory.Exists(extractPath)) return; if (!_directoryService.FileSystem.File.Exists(archivePath)) { diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 99fdd1400..fe7df8815 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -6,14 +6,12 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Xml; using API.Data.Metadata; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Services.Tasks.Scanner.Parser; -using API.Helpers; using Docnet.Core; using Docnet.Core.Converters; using Docnet.Core.Models; @@ -31,7 +29,6 @@ using VersOne.Epub.Options; using VersOne.Epub.Schema; namespace API.Services; - #nullable enable public interface IBookService @@ -71,50 +68,11 @@ public class BookService : IBookService private static readonly RecyclableMemoryStreamManager StreamManager = new (); private const string CssScopeClass = ".book-content"; private const string BookApiUrl = "book-resources?file="; - private readonly PdfComicInfoExtractor _pdfComicInfoExtractor; - - /// - /// Setup the most lenient book parsing options possible as people have some really bad epubs - /// public static readonly EpubReaderOptions BookReaderOptions = new() { PackageReaderOptions = new PackageReaderOptions { - IgnoreMissingToc = true, - SkipInvalidManifestItems = true, - }, - Epub2NcxReaderOptions = new Epub2NcxReaderOptions - { - IgnoreMissingContentForNavigationPoints = false - }, - SpineReaderOptions = new SpineReaderOptions - { - IgnoreMissingManifestItems = false - }, - BookCoverReaderOptions = new BookCoverReaderOptions - { - Epub2MetadataIgnoreMissingManifestItem = false - } - }; - - public static readonly EpubReaderOptions LenientBookReaderOptions = new() - { - PackageReaderOptions = new PackageReaderOptions - { - IgnoreMissingToc = true, - SkipInvalidManifestItems = true, - }, - Epub2NcxReaderOptions = new Epub2NcxReaderOptions - { - IgnoreMissingContentForNavigationPoints = false - }, - SpineReaderOptions = new SpineReaderOptions - { - IgnoreMissingManifestItems = false - }, - BookCoverReaderOptions = new BookCoverReaderOptions - { - Epub2MetadataIgnoreMissingManifestItem = true + IgnoreMissingToc = true } }; @@ -124,7 +82,6 @@ public class BookService : IBookService _directoryService = directoryService; _imageService = imageService; _mediaErrorService = mediaErrorService; - _pdfComicInfoExtractor = new PdfComicInfoExtractor(_logger, _mediaErrorService); } private static bool HasClickableHrefPart(HtmlNode anchor) @@ -241,6 +198,7 @@ public class BookService : IBookService } if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue; + //var bookFile = book.Content.AllFiles.Local[key]; var content = await bookFile.ReadContentAsBytesAsync(); importBuilder.Append(Encoding.UTF8.GetString(content)); } @@ -347,16 +305,8 @@ public class BookService : IBookService var imageFile = GetKeyForImage(book, image.Attributes[key].Value); image.Attributes.Remove(key); - - if (!imageFile.StartsWith("http")) - { - // UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx - image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile)); - } - else - { - image.Attributes.Add(key, imageFile); - } + // UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx + image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile)); // Add a custom class that the reader uses to ensure images stay within reader parent.AddClass("kavita-scale-width-container"); @@ -399,14 +349,11 @@ public class BookService : IBookService { // Check if any classes on the html node (some r2l books do this) and move them to body tag for scoping var htmlNode = doc.DocumentNode.SelectSingleNode("//html"); - if (htmlNode == null) return body.InnerHtml; + if (htmlNode == null || !htmlNode.Attributes.Contains("class")) return body.InnerHtml; var bodyClasses = body.Attributes.Contains("class") ? body.Attributes["class"].Value : string.Empty; - var htmlClasses = htmlNode.Attributes.Contains("class") ? htmlNode.Attributes["class"].Value : string.Empty; - - body.Attributes.Add("class", $"{htmlClasses} {bodyClasses}"); - - + var classes = htmlNode.Attributes["class"].Value + " " + bodyClasses; + body.Attributes.Add("class", $"{classes}"); // I actually need the body tag itself for the classes, so i will create a div and put the body stuff there. return $"
{body.InnerHtml}
"; } @@ -435,7 +382,7 @@ public class BookService : IBookService } } - var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link[@href]"); + var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link"); if (styleNodes != null) { foreach (var styleLinks in styleNodes) @@ -477,14 +424,13 @@ public class BookService : IBookService } } - private ComicInfo? GetEpubComicInfo(string filePath) + public ComicInfo? GetComicInfo(string filePath) { - EpubBookRef? epubBook = null; + if (!IsValidFile(filePath) || Parser.IsPdf(filePath)) return null; try { - epubBook = OpenEpubWithFallback(filePath, epubBook); - + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); var publicationDate = epubBook.Schema.Package.Metadata.Dates.Find(pDate => pDate.Event == "publication")?.Date; @@ -492,11 +438,10 @@ public class BookService : IBookService { publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date; } - var (year, month, day) = GetPublicationDate(publicationDate); var summary = epubBook.Schema.Package.Metadata.Descriptions.FirstOrDefault(); - var info = new ComicInfo + var info = new ComicInfo { Summary = string.IsNullOrEmpty(summary?.Description) ? string.Empty : summary.Description, Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers.Select(p => p.Publisher)), @@ -504,8 +449,7 @@ public class BookService : IBookService Day = day, Year = year, Title = epubBook.Title, - Genre = string.Join(",", - epubBook.Schema.Package.Metadata.Subjects.Select(s => s.Subject.ToLower().Trim())), + Genre = string.Join(",", epubBook.Schema.Package.Metadata.Subjects.Select(s => s.Subject.ToLower().Trim())), LanguageISO = ValidateLanguage(epubBook.Schema.Package.Metadata.Languages .Select(l => l.Language) .FirstOrDefault()) @@ -516,8 +460,7 @@ public class BookService : IBookService foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers) { if (string.IsNullOrEmpty(identifier.Identifier)) continue; - if (!string.IsNullOrEmpty(identifier.Scheme) && - identifier.Scheme.Equals("ISBN", StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty(identifier.Scheme) && identifier.Scheme.Equals("ISBN", StringComparison.InvariantCultureIgnoreCase)) { var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty); if (!ArticleNumberHelper.IsValidIsbn10(isbn) && !ArticleNumberHelper.IsValidIsbn13(isbn)) @@ -525,13 +468,11 @@ public class BookService : IBookService _logger.LogDebug("[BookService] {File} has invalid ISBN number", filePath); continue; } - info.Isbn = isbn; } - if ((!string.IsNullOrEmpty(identifier.Scheme) && - identifier.Scheme.Equals("URL", StringComparison.InvariantCultureIgnoreCase)) || - identifier.Identifier.StartsWith("url:")) + if ((!string.IsNullOrEmpty(identifier.Scheme) && identifier.Scheme.Equals("URL", StringComparison.InvariantCultureIgnoreCase)) || + identifier.Identifier.StartsWith("url:")) { var url = identifier.Identifier.Replace("url:", string.Empty); weblinks.Add(url.Trim()); @@ -561,7 +502,6 @@ public class BookService : IBookService { info.SeriesSort = metadataItem.Content; } - break; case "calibre:series_index": info.Volume = metadataItem.Content; @@ -581,7 +521,6 @@ public class BookService : IBookService { info.SeriesSort = metadataItem.Content; } - break; case "collection-type": // These look to be genres from https://manual.calibre-ebook.com/sub_groups.html or can be "series" @@ -607,14 +546,15 @@ public class BookService : IBookService ExtractSortTitle(metadataItem, epubBook, info); } + break; } } // If this is a single book and not a collection, set publication status to Completed - if (string.IsNullOrEmpty(info.Volume) && - Parser.ParseVolume(filePath, LibraryType.Manga).Equals(Parser.LooseLeafVolume)) + if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath).Equals(Parser.DefaultVolume)) { + info.Number = "1"; info.Count = 1; } @@ -622,69 +562,28 @@ public class BookService : IBookService info.Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator))); - var hasVolumeInSeries = !Parser.ParseVolume(info.Title, LibraryType.Manga) - .Equals(Parser.LooseLeafVolume); + var hasVolumeInSeries = !Parser.ParseVolume(info.Title) + .Equals(Parser.DefaultVolume); - if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && - (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series))) + if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series))) { // This is likely a light novel for which we can set series from parsed title - info.Series = Parser.ParseSeries(info.Title, LibraryType.Manga); - info.Volume = Parser.ParseVolume(info.Title, LibraryType.Manga); + info.Series = Parser.ParseSeries(info.Title); + info.Volume = Parser.ParseVolume(info.Title); } return info; } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata: {FilePath}", filePath); + _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata"); _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception parsing metadata", ex); } - finally - { - epubBook?.Dispose(); - } return null; } - private EpubBookRef? OpenEpubWithFallback(string filePath, EpubBookRef? epubBook) - { - try - { - epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); - } - catch (Exception ex) - { - _logger.LogWarning(ex, - "[GetComicInfo] There was an exception parsing metadata, falling back to a more lenient parsing method: {FilePath}", - filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, - "There was an exception parsing metadata", ex); - } - finally - { - epubBook ??= EpubReader.OpenBook(filePath, LenientBookReaderOptions); - } - - return epubBook; - } - - public ComicInfo? GetComicInfo(string filePath) - { - if (!IsValidFile(filePath)) return null; - - if (Parser.IsPdf(filePath)) - { - return _pdfComicInfoExtractor.GetComicInfo(filePath); - } - else - { - return GetEpubComicInfo(filePath); - } - } - private static void ExtractSortTitle(EpubMetadataMeta metadataItem, EpubBookRef epubBook, ComicInfo info) { var titleId = metadataItem.Refines?.Replace("#", string.Empty); @@ -711,6 +610,7 @@ public class BookService : IBookService item.Property == "display-seq" && item.Refines == metadataItem.Refines); if (count == null || count.Content == "0") { + // TODO: Rewrite this to use a StringBuilder // Treat this as a Collection info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + readingListElem.Title.Replace(',', '_'); @@ -734,8 +634,6 @@ public class BookService : IBookService return; case "aut": case "author": - case "creator": - case "cre": info.Writer += AppendAuthor(person); return; case "pbl": @@ -772,7 +670,7 @@ public class BookService : IBookService var month = 0; var day = 0; if (string.IsNullOrEmpty(publicationDate)) return (year, month, day); - switch (DateTime.TryParse(publicationDate, CultureInfo.InvariantCulture, out var date)) + switch (DateTime.TryParse(publicationDate, out var date)) { case true: year = date.Year; @@ -787,7 +685,7 @@ public class BookService : IBookService return (year, month, day); } - public static string ValidateLanguage(string? language) + private static string ValidateLanguage(string? language) { if (string.IsNullOrEmpty(language)) return string.Empty; @@ -827,7 +725,7 @@ public class BookService : IBookService return docReader.GetPageCount(); } - using var epubBook = EpubReader.OpenBook(filePath, LenientBookReaderOptions); + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); return epubBook.GetReadingOrder().Count; } catch (Exception ex) @@ -842,6 +740,8 @@ public class BookService : IBookService private static string EscapeTags(string content) { + // content = StartingScriptTag().Replace(content, ""); + // content = StartingTitleTag().Replace(content, ""); content = Regex.Replace(content, @")", "", RegexOptions.None, Parser.RegexTimeout); content = Regex.Replace(content, @")", "", RegexOptions.None, Parser.RegexTimeout); return content; @@ -881,11 +781,11 @@ public class BookService : IBookService /// public ParserInfo? ParseInfo(string filePath) { - if (!Parser.IsEpub(filePath) || !_directoryService.FileSystem.File.Exists(filePath)) return null; + if (!Parser.IsEpub(filePath)) return null; try { - using var epubBook = EpubReader.OpenBook(filePath, LenientBookReaderOptions); + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); // // @@ -948,8 +848,8 @@ public class BookService : IBookService Format = MangaFormat.Epub, Filename = Path.GetFileName(filePath), Title = specialName?.Trim() ?? string.Empty, - FullFilePath = Parser.NormalizePath(filePath), - IsSpecial = Parser.HasSpecialMarker(filePath), + FullFilePath = filePath, + IsSpecial = false, Series = series.Trim(), SeriesSort = series.Trim(), Volumes = seriesIndex @@ -970,10 +870,10 @@ public class BookService : IBookService Format = MangaFormat.Epub, Filename = Path.GetFileName(filePath), Title = epubBook.Title.Trim(), - FullFilePath = Parser.NormalizePath(filePath), - IsSpecial = Parser.HasSpecialMarker(filePath), + FullFilePath = filePath, + IsSpecial = false, Series = epubBook.Title.Trim(), - Volumes = Parser.LooseLeafVolume, + Volumes = Parser.DefaultVolume, }; } catch (Exception ex) @@ -987,7 +887,7 @@ public class BookService : IBookService } /// - /// Extracts a pdf into images to a target directory. Uses multithreaded implementation since docnet is slow normally. + /// Extracts a pdf into images to a target directory. Uses multi-threaded implementation since docnet is slow normally. /// /// /// @@ -1035,9 +935,8 @@ public class BookService : IBookService /// /// /// - private static string? CoalesceKey(EpubBookRef book, IReadOnlyDictionary mappings, string? key) + private static string CoalesceKey(EpubBookRef book, IReadOnlyDictionary mappings, string key) { - if (string.IsNullOrEmpty(key)) return key; if (mappings.ContainsKey(CleanContentKeys(key))) return key; // Fallback to searching for key (bad epub metadata) @@ -1047,7 +946,7 @@ public class BookService : IBookService key = correctedKey; } - var stepsBack = CountParentDirectory(book.Content.NavigationHtmlFile?.FilePath); + var stepsBack = CountParentDirectory(book.Content.NavigationHtmlFile?.FilePath); // FileName -> FilePath if (mappings.TryGetValue(key, out _)) { return key; @@ -1089,7 +988,7 @@ public class BookService : IBookService /// public async Task> GenerateTableOfContents(Chapter chapter) { - using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, LenientBookReaderOptions); + using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookReaderOptions); var mappings = await CreateKeyToPageMappingAsync(book); var navItems = await book.GetNavigationAsync(); @@ -1143,6 +1042,8 @@ public class BookService : IBookService // TODO: We may want to check if there is a toc.ncs file to better handle nested toc // We could do a fallback first with ol/lis + //var sections = doc.DocumentNode.SelectNodes("//ol"); + //if (sections == null) @@ -1217,7 +1118,7 @@ public class BookService : IBookService /// All exceptions throw this public async Task GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl) { - using var book = await EpubReader.OpenBookAsync(cachedEpubPath, LenientBookReaderOptions); + using var book = await EpubReader.OpenBookAsync(cachedEpubPath, BookReaderOptions); var mappings = await CreateKeyToPageMappingAsync(book); var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl; @@ -1319,13 +1220,13 @@ public class BookService : IBookService return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat, size); } - using var epubBook = EpubReader.OpenBook(fileFilePath, LenientBookReaderOptions); + using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions); try { // Try to get the cover image from OPF file, if not set, try to parse it from all the files, then result to the first one. var coverImageContent = epubBook.Content.Cover - ?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) + ?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath ?? epubBook.Content.Images.Local.FirstOrDefault(); if (coverImageContent == null) return string.Empty; @@ -1337,7 +1238,7 @@ public class BookService : IBookService { _logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, - "There was a critical error and prevented thumbnail generation", ex); + "There was a critical error and prevented thumbnail generation", ex); // TODO: Localize this } return string.Empty; diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index 4cd77ddd9..7ff7cd0ad 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -7,14 +7,11 @@ using API.Data; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; -using API.Extensions; using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable - public interface IBookmarkService { Task DeleteBookmarkFiles(IEnumerable bookmarks); @@ -91,13 +88,6 @@ public class BookmarkService : IBookmarkService var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId); if (bookmark == null) return; - // Validate the bookmark isn't already in target format - if (bookmark.FileName.EndsWith(encodeFormat.GetExtension())) - { - // Nothing to ddo - return; - } - bookmark.FileName = await _mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); _unitOfWork.UserRepository.Update(bookmark); @@ -145,7 +135,7 @@ public class BookmarkService : IBookmarkService _unitOfWork.UserRepository.Add(bookmark); await _unitOfWork.CommitAsync(); - if (settings.EncodeMediaAs != EncodeFormat.PNG) + if (settings.EncodeMediaAs == EncodeFormat.WEBP) { // Enqueue a task to convert the bookmark to webP BackgroundJob.Enqueue(() => ConvertBookmarkToEncoding(bookmark.Id)); diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 283d4b1ac..c19797b22 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.DTOs.Reader; @@ -53,8 +51,6 @@ public class CacheService : ICacheService private readonly IReadingItemService _readingItemService; private readonly IBookmarkService _bookmarkService; - private static readonly ConcurrentDictionary ExtractLocks = new(); - public CacheService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IReadingItemService readingItemService, IBookmarkService bookmarkService) @@ -80,6 +76,7 @@ public class CacheService : ICacheService /// public IEnumerable GetCachedFileDimensions(string cachePath) { + var sw = Stopwatch.StartNew(); var files = _directoryService.GetFilesWithExtension(cachePath, Tasks.Scanner.Parser.Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) .ToArray(); @@ -117,6 +114,7 @@ public class CacheService : ICacheService Cache.MaxFiles = originalCacheSize; } + _logger.LogDebug("File Dimensions call for {Length} images took {Time}ms", dimensions.Count, sw.ElapsedMilliseconds); return dimensions; } @@ -168,42 +166,11 @@ public class CacheService : ICacheService var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); var extractPath = GetCachePath(chapterId); - var extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1)); + if (_directoryService.Exists(extractPath)) return chapter; + var files = chapter?.Files.ToList(); + ExtractChapterFiles(extractPath, files, extractPdfToImages); - await extractLock.WaitAsync(); - try { - if (_directoryService.Exists(extractPath)) - { - if (extractPdfToImages) - { - var pdfImages = _directoryService.GetFiles(extractPath, - Tasks.Scanner.Parser.Parser.ImageFileExtensions); - if (pdfImages.Any()) - { - return chapter; - } - } - else - { - // Do an explicit check for files since rarely a "permission denied" error on deleting - // the file can occur, thus leaving an empty folder and we would never re-cache the files. - if (_directoryService.GetFiles(extractPath).Any()) - { - return chapter; - } - - // Delete the extractPath as ExtractArchive will return if the directory already exists - _directoryService.ClearAndDeleteDirectory(extractPath); - } - } - - var files = chapter?.Files.ToList(); - ExtractChapterFiles(extractPath, files, extractPdfToImages); - } finally { - extractLock.Release(); - } - - return chapter; + return chapter; } /// @@ -216,33 +183,16 @@ public class CacheService : ICacheService /// public void ExtractChapterFiles(string extractPath, IReadOnlyList? files, bool extractPdfImages = false) { - if (files == null || files.Count == 0) return; + if (files == null) return; var removeNonImages = true; var fileCount = files.Count; var extraPath = string.Empty; var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath); - if (files[0].Format == MangaFormat.Image) + if (files.Count > 0 && files[0].Format == MangaFormat.Image) { - // Check if all the files are Images. If so, do a directory copy, else do the normal copy - if (files.All(f => f.Format == MangaFormat.Image)) - { - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath); - } - else - { - foreach (var file in files) - { - if (fileCount > 1) - { - extraPath = file.Id + string.Empty; - } - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), MangaFormat.Image, files.Count); - } - _directoryService.Flatten(extractDi.FullName); - } - + _readingItemService.Extract(files[0].FilePath, extractPath, MangaFormat.Image, files.Count); + _directoryService.Flatten(extractDi.FullName); } foreach (var file in files) @@ -343,7 +293,7 @@ public class CacheService : ICacheService var path = GetCachePath(chapterId); // NOTE: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) - //.OrderByNatural(Path.GetFileNameWithoutExtension) // This is already done in GetPageFromFiles + .OrderByNatural(Path.GetFileNameWithoutExtension) .ToArray(); return GetPageFromFiles(files, page); diff --git a/API/Services/CollectionTagService.cs b/API/Services/CollectionTagService.cs index a73c0cea2..b024d687a 100644 --- a/API/Services/CollectionTagService.cs +++ b/API/Services/CollectionTagService.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; -using API.DTOs.Collection; +using API.Data.Repositories; +using API.DTOs.CollectionTags; using API.Entities; -using API.Extensions; -using API.Services.Plus; +using API.Entities.Metadata; +using API.Helpers.Builders; using API.SignalR; using Kavita.Common; @@ -15,9 +16,15 @@ namespace API.Services; public interface ICollectionTagService { - Task DeleteTag(int tagId, AppUser user); - Task UpdateTag(AppUserCollectionDto dto, int userId); - Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds); + Task TagExistsByName(string name); + Task DeleteTag(CollectionTag tag); + Task UpdateTag(CollectionTagDto dto); + Task AddTagToSeries(CollectionTag? tag, IEnumerable seriesIds); + Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds); + Task GetTagOrCreate(int tagId, string title); + void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata); + CollectionTag CreateTag(string title); + Task RemoveTagsWithoutSeries(); } @@ -32,49 +39,42 @@ public class CollectionTagService : ICollectionTagService _eventHub = eventHub; } - public async Task DeleteTag(int tagId, AppUser user) + /// + /// Checks if a collection exists with the name + /// + /// If empty or null, will return true as that is invalid + /// + public async Task TagExistsByName(string name) { - var collectionTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(tagId); - if (collectionTag == null) return true; - - user.Collections.Remove(collectionTag); - - if (!_unitOfWork.HasChanges()) return true; + if (string.IsNullOrEmpty(name.Trim())) return true; + return await _unitOfWork.CollectionTagRepository.TagExists(name); + } + public async Task DeleteTag(CollectionTag tag) + { + _unitOfWork.CollectionTagRepository.Remove(tag); return await _unitOfWork.CommitAsync(); } - - public async Task UpdateTag(AppUserCollectionDto dto, int userId) + public async Task UpdateTag(CollectionTagDto dto) { - var existingTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(dto.Id); + var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id); if (existingTag == null) throw new KavitaException("collection-doesnt-exist"); - if (existingTag.AppUserId != userId) throw new KavitaException("access-denied"); var title = dto.Title.Trim(); if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required"); - - // Ensure the title doesn't exist on the user's account already - if (!title.Equals(existingTag.Title) && await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId)) + if (!title.Equals(existingTag.Title) && await TagExistsByName(dto.Title)) throw new KavitaException("collection-tag-duplicate"); - existingTag.Items ??= []; - if (existingTag.Source == ScrobbleProvider.Kavita) - { - existingTag.Title = title; - existingTag.NormalizedTitle = dto.Title.ToNormalized(); - } - - var roles = await _unitOfWork.UserRepository.GetRoles(userId); - if (roles.Contains(PolicyConstants.AdminRole) || roles.Contains(PolicyConstants.PromoteRole)) - { - existingTag.Promoted = dto.Promoted; - } + existingTag.SeriesMetadatas ??= new List(); + existingTag.Title = title; + existingTag.NormalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(dto.Title); + existingTag.Promoted = dto.Promoted; existingTag.CoverImageLocked = dto.CoverImageLocked; _unitOfWork.CollectionTagRepository.Update(existingTag); // Check if Tag has updated (Summary) - var summary = (dto.Summary ?? string.Empty).Trim(); + var summary = dto.Summary.Trim(); if (existingTag.Summary == null || !existingTag.Summary.Equals(summary)) { existingTag.Summary = summary; @@ -96,31 +96,89 @@ public class CollectionTagService : ICollectionTagService } /// - /// Removes series from Collection tag. Will recalculate max age rating. + /// Adds a set of Series to a Collection /// - /// + /// A full Tag /// /// - public async Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds) + public async Task AddTagToSeries(CollectionTag? tag, IEnumerable seriesIds) { if (tag == null) return false; + var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(seriesIds); + foreach (var metadata in metadatas) + { + AddTagToSeriesMetadata(tag, metadata); + } - tag.Items ??= []; - tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList(); + if (!_unitOfWork.HasChanges()) return true; + return await _unitOfWork.CommitAsync(); + } - if (tag.Items.Count == 0) + /// + /// Adds a collection tag to a SeriesMetadata + /// + /// Does not commit + /// + /// + /// + public void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata) + { + if (tag == null) return; + metadata.CollectionTags ??= new List(); + if (metadata.CollectionTags.Any(t => t.NormalizedTitle.Equals(tag.NormalizedTitle, StringComparison.InvariantCulture))) return; + + metadata.CollectionTags.Add(tag); + if (metadata.Id != 0) + { + _unitOfWork.SeriesMetadataRepository.Update(metadata); + } + } + + public async Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds) + { + if (tag == null) return false; + tag.SeriesMetadatas ??= new List(); + foreach (var seriesIdToRemove in seriesIds) + { + tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove)); + } + + + if (tag.SeriesMetadatas.Count == 0) { _unitOfWork.CollectionTagRepository.Remove(tag); } if (!_unitOfWork.HasChanges()) return true; - var result = await _unitOfWork.CommitAsync(); - if (tag.Items.Count > 0) - { - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(tag); - } + return await _unitOfWork.CommitAsync(); + } - return result; + /// + /// Tries to fetch the full tag, else returns a new tag. Adds to tracking but does not commit + /// + /// + /// + /// + public async Task GetTagOrCreate(int tagId, string title) + { + return await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata) ?? CreateTag(title); + } + + /// + /// This just creates the entity and adds to tracking. Use for checks of duplication. + /// + /// + /// + public CollectionTag CreateTag(string title) + { + var tag = new CollectionTagBuilder(title).Build(); + _unitOfWork.CollectionTagRepository.Add(tag); + return tag; + } + + public async Task RemoveTagsWithoutSeries() + { + return await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries() > 0; } } diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs index ddaf93b64..02cdddd62 100644 --- a/API/Services/DeviceService.cs +++ b/API/Services/DeviceService.cs @@ -107,10 +107,6 @@ public class DeviceService : IDeviceService public async Task SendTo(IReadOnlyList chapterIds, int deviceId) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!settings.IsEmailSetupForSendToDevice()) - throw new KavitaException("send-to-kavita-email"); - var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId); if (device == null) throw new KavitaException("device-doesnt-exist"); @@ -118,22 +114,10 @@ public class DeviceService : IDeviceService if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle) throw new KavitaException("send-to-permission"); - // If the size of the files is too big - if (files.Sum(f => f.Bytes) >= settings.SmtpConfig.SizeLimit) - throw new KavitaException("send-to-size-limit"); - - - try - { - device.UpdateLastUsed(); - _unitOfWork.DeviceRepository.Update(device); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue updating device last used time"); - } + device.UpdateLastUsed(); + _unitOfWork.DeviceRepository.Update(device); + await _unitOfWork.CommitAsync(); var success = await _emailService.SendFilesToEmail(new SendToDto() { DestinationEmail = device.EmailAddress!, diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 7e308d92e..78102e126 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using API.DTOs.System; using API.Entities.Enums; using API.Extensions; -using API.Services.Tasks.Scanner.Parser; using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; @@ -27,22 +26,11 @@ public interface IDirectoryService string SiteThemeDirectory { get; } string FaviconDirectory { get; } string LocalizationDirectory { get; } - string CustomizedTemplateDirectory { get; } - string TemplateDirectory { get; } - string PublisherDirectory { get; } - /// - /// Used for caching documents that may need to stay on disk for more than a day - /// - string LongTermCacheDirectory { get; } /// /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. /// string BookmarkDirectory { get; } /// - /// Used for random files needed, like images to check against, list of countries, etc - /// - string AssetsDirectory { get; } - /// /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. /// /// Absolute path of directory to scan. @@ -63,13 +51,10 @@ public interface IDirectoryService bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, IList filePaths); - string? FindLowestDirectoriesFromFiles(IList libraryFolders, - IList filePaths); IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); bool ExistOrCreate(string directoryPath); void DeleteFiles(IEnumerable files); - void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); void RemoveNonImages(string directoryName); void Flatten(string directoryName); Task CheckWriteAccess(string directoryName); @@ -78,13 +63,14 @@ public interface IDirectoryService SearchOption searchOption = SearchOption.TopDirectoryOnly); IEnumerable GetDirectories(string folderPath); IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); - IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null); string GetParentDirectoryName(string fileOrFolder); - IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories); + IList ScanFiles(string folderPath, GlobMatcher? matcher = null); DateTime GetLastWriteTime(string folderPath); + GlobMatcher? CreateMatcherFromFile(string filePath); } public class DirectoryService : IDirectoryService { + public const string KavitaIgnoreFile = ".kavitaignore"; public IFileSystem FileSystem { get; } public string CacheDirectory { get; } public string CoverImageDirectory { get; } @@ -92,22 +78,19 @@ public class DirectoryService : IDirectoryService public string TempDirectory { get; } public string ConfigDirectory { get; } public string BookmarkDirectory { get; } - public string AssetsDirectory { get; } public string SiteThemeDirectory { get; } public string FaviconDirectory { get; } public string LocalizationDirectory { get; } - public string CustomizedTemplateDirectory { get; } - public string TemplateDirectory { get; } - public string PublisherDirectory { get; } - public string LongTermCacheDirectory { get; } private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; private static readonly Regex ExcludeDirectories = new Regex( - @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle|\.yacreaderlibrary", - MatchOptions, Parser.RegexTimeout); + @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle", + MatchOptions, + Tasks.Scanner.Parser.Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", - MatchOptions, Parser.RegexTimeout); + MatchOptions, + Tasks.Scanner.Parser.Parser.RegexTimeout); public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); public DirectoryService(ILogger logger, IFileSystem fileSystem) @@ -126,21 +109,11 @@ public class DirectoryService : IDirectoryService ExistOrCreate(TempDirectory); BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks"); ExistOrCreate(BookmarkDirectory); - AssetsDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "Assets"); - ExistOrCreate(AssetsDirectory); SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes"); ExistOrCreate(SiteThemeDirectory); FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons"); ExistOrCreate(FaviconDirectory); LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N"); - CustomizedTemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "templates"); - ExistOrCreate(CustomizedTemplateDirectory); - TemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "EmailTemplates"); - ExistOrCreate(TemplateDirectory); - PublisherDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "images", "publishers"); - ExistOrCreate(PublisherDirectory); - LongTermCacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache-long"); - ExistOrCreate(LongTermCacheDirectory); } /// @@ -148,38 +121,22 @@ public class DirectoryService : IDirectoryService /// /// This will always exclude patterns /// Directory to search - /// Regex version of search pattern (e.g., \.mp3|\.mp4). Defaults to * meaning all files. + /// Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly /// List of file paths public IEnumerable GetFilesWithCertainExtensions(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { - // If directory doesn't exist, exit the iterator with no results - if (!FileSystem.Directory.Exists(path)) - yield break; + if (!FileSystem.Directory.Exists(path)) return ImmutableList.Empty; + var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); - // Compile the regex pattern for faster repeated matching - var reSearchPattern = new Regex(searchPatternExpression, - RegexOptions.IgnoreCase | RegexOptions.Compiled, - Parser.RegexTimeout); - - // Enumerate files in the directory and apply filters - foreach (var file in FileSystem.Directory.EnumerateFiles(path, "*", searchOption)) - { - var fileName = FileSystem.Path.GetFileName(file); - var fileExtension = FileSystem.Path.GetExtension(file); - - // Check if the file matches the pattern and exclude macOS metadata files - if (reSearchPattern.IsMatch(fileExtension) && !fileName.StartsWith(Parser.MacOsMetadataFileStartsWith)) - { - yield return file; - } - } + return FileSystem.Directory.EnumerateFiles(path, "*", searchOption) + .Where(file => + reSearchPattern.IsMatch(FileSystem.Path.GetExtension(file)) && !FileSystem.Path.GetFileName(file).StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)); } - /// /// Returns a list of folders from end of fullPath to rootPath. If a file is passed at the end of the fullPath, it will be ignored. /// @@ -201,6 +158,8 @@ public class DirectoryService : IDirectoryService rootPath = rootPath.Replace(FileSystem.Path.DirectorySeparatorChar, FileSystem.Path.AltDirectorySeparatorChar); } + + var path = fullPath.EndsWith(separator) ? fullPath.Substring(0, fullPath.Length - 1) : fullPath; var root = rootPath.EndsWith(separator) ? rootPath.Substring(0, rootPath.Length - 1) : rootPath; var paths = new List(); @@ -241,34 +200,25 @@ public class DirectoryService : IDirectoryService /// public IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { - if (!FileSystem.Directory.Exists(path)) - yield break; // Use yield break to exit the iterator early + if (!FileSystem.Directory.Exists(path)) return ImmutableList.Empty; - Regex? reSearchPattern = null; - if (!string.IsNullOrEmpty(fileNameRegex)) + if (fileNameRegex != string.Empty) { - // Compile the regex for better performance when used frequently - reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled, Tasks.Scanner.Parser.Parser.RegexTimeout); + var reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase, + Tasks.Scanner.Parser.Parser.RegexTimeout); + return FileSystem.Directory.EnumerateFiles(path, "*", searchOption) + .Where(file => + { + var fileName = FileSystem.Path.GetFileName(file); + return reSearchPattern.IsMatch(fileName) && + !fileName.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith); + }); } - // Enumerate files lazily - foreach (var file in FileSystem.Directory.EnumerateFiles(path, "*", searchOption)) - { - var fileName = FileSystem.Path.GetFileName(file); - - // Exclude macOS metadata files - if (fileName.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)) - continue; - - // If a regex is provided, match the file name against it - if (reSearchPattern != null && !reSearchPattern.IsMatch(fileName)) - continue; - - yield return file; // Yield each matching file as it's found - } + return FileSystem.Directory.EnumerateFiles(path, "*", searchOption).Where(file => + !FileSystem.Path.GetFileName(file).StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)); } - /// /// Copies a file into a directory. Does not maintain parent folder of file. /// Will create target directory if doesn't exist. Automatically overwrites what is there. @@ -364,7 +314,7 @@ public class DirectoryService : IDirectoryService return GetFilesWithCertainExtensions(path, searchPatternExpression).ToArray(); } - return !FileSystem.Directory.Exists(path) ? [] : FileSystem.Directory.GetFiles(path); + return !FileSystem.Directory.Exists(path) ? Array.Empty() : FileSystem.Directory.GetFiles(path); } /// @@ -419,19 +369,16 @@ public class DirectoryService : IDirectoryService /// public void ClearDirectory(string directoryPath) { - directoryPath = directoryPath.Replace(Environment.NewLine, string.Empty); var di = FileSystem.DirectoryInfo.New(directoryPath); if (!di.Exists) return; try { foreach (var file in di.EnumerateFiles()) { - if (!file.Exists) continue; file.Delete(); } foreach (var dir in di.EnumerateDirectories()) { - if (!dir.Exists) continue; dir.Delete(true); } } @@ -628,63 +575,6 @@ public class DirectoryService : IDirectoryService return dirs; } - /// - /// Finds the lowest directory from a set of file paths. Does not return the root path, will always select the lowest non-root path. - /// - /// If the file paths do not contain anything from libraryFolders, this returns null. - /// List of top level folders which files belong to - /// List of file paths that belong to libraryFolders - /// Lowest non-root path, or null if not found - public string? FindLowestDirectoriesFromFiles(IList libraryFolders, IList filePaths) - { - // Normalize the file paths only once - var normalizedFilePaths = filePaths.Select(Parser.NormalizePath).ToList(); - - // Use a list to store all directories for comparison - var dirs = new List(); - - // Iterate through each library folder and collect matching directories - foreach (var normalizedFolder in libraryFolders.Select(Parser.NormalizePath)) - { - foreach (var file in normalizedFilePaths) - { - // If the file path contains the folder path, get its directory - if (!file.Contains(normalizedFolder)) continue; - - var lowestPath = Path.GetDirectoryName(file); - if (!string.IsNullOrEmpty(lowestPath)) - { - dirs.Add(Parser.NormalizePath(lowestPath)); // Add to list - } - } - } - - if (dirs.Count == 0) - { - return null; // No directories found - } - - // Now find the deepest common directory among all paths - var commonPath = dirs.Aggregate(GetDeepestCommonPath); // Use new method to get deepest path - - // Return the common path if it exists and is not one of the root directories - return libraryFolders.Any(folder => commonPath == Parser.NormalizePath(folder)) ? null : commonPath; - } - - public static string GetDeepestCommonPath(string path1, string path2) - { - var parts1 = path1.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - var parts2 = path2.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - - // Get the longest matching parts, ensuring that deeper parts in hierarchy are considered - var commonParts = parts1.Zip(parts2, (p1, p2) => p1 == p2 ? p1 : null) - .TakeWhile(part => part != null) - .ToArray(); - - return Parser.NormalizePath(string.Join(Path.DirectorySeparatorChar.ToString(), commonParts)); - } - - /// /// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope. /// @@ -716,18 +606,17 @@ public class DirectoryService : IDirectoryService /// Returns all directories, including subdirectories. Automatically excludes directories that shouldn't be in scope. /// /// - /// /// - public IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null) + public IEnumerable GetAllDirectories(string folderPath) { if (!FileSystem.Directory.Exists(folderPath)) return ImmutableArray.Empty; var directories = new List(); - var foundDirs = GetDirectories(folderPath, matcher); + var foundDirs = GetDirectories(folderPath); foreach (var foundDir in foundDirs) { directories.Add(foundDir); - directories.AddRange(GetAllDirectories(foundDir, matcher)); + directories.AddRange(GetAllDirectories(foundDir)); } return directories; @@ -751,81 +640,92 @@ public class DirectoryService : IDirectoryService } /// - /// Scans a directory by utilizing a recursive folder search. + /// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns /// /// - /// /// - /// Pass TopDirectories /// - public IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, - SearchOption searchOption = SearchOption.AllDirectories) + public IList ScanFiles(string folderPath, GlobMatcher? matcher = null) { + _logger.LogDebug("[ScanFiles] called on {Path}", folderPath); var files = new List(); - if (!Exists(folderPath)) return files; - if (searchOption == SearchOption.AllDirectories) + var potentialIgnoreFile = FileSystem.Path.Join(folderPath, KavitaIgnoreFile); + if (matcher == null) { - - // Stack to hold directories to process - var directoriesToProcess = new Stack(); - directoriesToProcess.Push(folderPath); - - while (directoriesToProcess.Count > 0) - { - var currentDirectory = directoriesToProcess.Pop(); - - // Get files from the current directory - var filesInCurrentDirectory = GetFilesWithCertainExtensions(currentDirectory, fileTypes); - files.AddRange(filesInCurrentDirectory); - - // Get subdirectories and add them to the stack - var subdirectories = GetDirectories(currentDirectory, matcher); - foreach (var subdirectory in subdirectories) - { - directoriesToProcess.Push(subdirectory); - } - } + matcher = CreateMatcherFromFile(potentialIgnoreFile); } else { - // If TopDirectoryOnly is specified, only get files in the specified folder - var filesInCurrentDirectory = GetFilesWithCertainExtensions(folderPath, fileTypes); - files.AddRange(filesInCurrentDirectory); + matcher.Merge(CreateMatcherFromFile(potentialIgnoreFile)); } - // Filter out unwanted files based on matcher if provided - if (matcher != null) + + var directories = GetDirectories(folderPath, matcher); + + foreach (var directory in directories) { - files = files.Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)).ToList(); + files.AddRange(ScanFiles(directory, matcher)); + } + + + // Get the matcher from either ignore or global (default setup) + if (matcher == null) + { + files.AddRange(GetFilesWithCertainExtensions(folderPath, Tasks.Scanner.Parser.Parser.SupportedExtensions)); + } + else + { + var foundFiles = GetFilesWithCertainExtensions(folderPath, + Tasks.Scanner.Parser.Parser.SupportedExtensions) + .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)); + files.AddRange(foundFiles); } return files; } - /// /// Recursively scans a folder and returns the max last write time on any folders and files /// - /// If the folder is empty or non-existent, this will return MaxValue for a DateTime + /// If the folder is empty, this will return MaxValue for a DateTime /// /// Max Last Write Time public DateTime GetLastWriteTime(string folderPath) { - if (!FileSystem.Directory.Exists(folderPath)) return DateTime.MaxValue; - + if (!FileSystem.Directory.Exists(folderPath)) throw new IOException($"{folderPath} does not exist"); var fileEntries = FileSystem.Directory.GetFileSystemEntries(folderPath, "*.*", SearchOption.AllDirectories); if (fileEntries.Length == 0) return DateTime.MaxValue; + return fileEntries.Max(path => FileSystem.File.GetLastWriteTime(path)); + } - // Find the max last write time of the files - var maxFiles = fileEntries.Max(path => FileSystem.File.GetLastWriteTime(path)); + /// + /// Generates a GlobMatcher from a .kavitaignore file found at path. Returns null otherwise. + /// + /// + /// + public GlobMatcher? CreateMatcherFromFile(string filePath) + { + if (!FileSystem.File.Exists(filePath)) + { + return null; + } - // Get the last write time of the directory itself - var directoryLastWriteTime = FileSystem.Directory.GetLastWriteTime(folderPath); + // Read file in and add each line to Matcher + var lines = FileSystem.File.ReadAllLines(filePath); + if (lines.Length == 0) + { + return null; + } - // Use comparison to get the max DateTime value - return directoryLastWriteTime > maxFiles ? directoryLastWriteTime : maxFiles; + GlobMatcher matcher = new(); + foreach (var line in lines.Where(s => !string.IsNullOrEmpty(s))) + { + matcher.AddExclude(line); + } + + return matcher; } @@ -938,27 +838,6 @@ public class DirectoryService : IDirectoryService } } - public void CopyFile(string sourcePath, string destinationPath, bool overwrite = true) - { - if (!File.Exists(sourcePath)) - { - throw new FileNotFoundException("Source file not found", sourcePath); - } - - var destinationDirectory = Path.GetDirectoryName(destinationPath); - if (string.IsNullOrEmpty(destinationDirectory)) - { - throw new ArgumentException("Destination path does not contain a directory", nameof(destinationPath)); - } - - if (!Directory.Exists(destinationDirectory)) - { - FileSystem.Directory.CreateDirectory(destinationDirectory); - } - - FileSystem.File.Copy(sourcePath, destinationPath, overwrite); - } - /// /// Returns the human-readable file size for an arbitrary, 64-bit file size /// The default format is "0.## XB", e.g. "4.2 KB" or "1.43 GB" @@ -1112,23 +991,4 @@ public class DirectoryService : IDirectoryService FlattenDirectory(root, subDirectory, ref directoryIndex); } } - - /// - /// If the file is locked or not existing - /// - /// - /// - public static bool IsFileLocked(string filePath) - { - try - { - if (!File.Exists(filePath)) return false; - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None); - return false; // If this works, the file is not locked - } - catch (IOException) - { - return true; // File is locked by another process - } - } } diff --git a/API/Services/DownloadService.cs b/API/Services/DownloadService.cs index 8a8cff1da..a8dfd5d50 100644 --- a/API/Services/DownloadService.cs +++ b/API/Services/DownloadService.cs @@ -35,12 +35,6 @@ public class DownloadService : IDownloadService // Figures out what the content type should be based on the file name. if (!_fileTypeProvider.TryGetContentType(filepath, out var contentType)) { - if (contentType == null) - { - // Get extension - contentType = Path.GetExtension(filepath); - } - contentType = Path.GetExtension(filepath).ToLowerInvariant() switch { ".cbz" => "application/x-cbz", diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index 35cfa7b04..e6f9c1684 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -1,141 +1,76 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.IO; using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; -using System.Web; using API.Data; using API.DTOs.Email; -using API.Entities; -using API.Services.Plus; +using API.Entities.Enums; +using Flurl; +using Flurl.Http; using Kavita.Common; using Kavita.Common.EnvironmentInfo; -using Kavita.Common.Extensions; -using MailKit.Security; +using Kavita.Common.Helpers; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using MimeKit; -using MimeTypes; namespace API.Services; -#nullable enable - -internal class EmailOptionsDto -{ - public required IList ToEmails { get; set; } - public required string Subject { get; set; } - public required string Body { get; set; } - public required string Preheader { get; set; } - public IList>? PlaceHolders { get; set; } - /// - /// Filenames to attach - /// - public IList? Attachments { get; set; } - public int? ToUserId { get; set; } - public required string Template { get; set; } -} public interface IEmailService { - Task SendInviteEmail(ConfirmationEmailDto data); + Task SendConfirmationEmail(ConfirmationEmailDto data); Task CheckIfAccessible(string host); - Task SendForgotPasswordEmail(PasswordResetEmailDto dto); + Task SendMigrationEmail(EmailMigrationDto data); + Task SendPasswordResetEmail(PasswordResetEmailDto data); Task SendFilesToEmail(SendToDto data); - Task SendTestEmail(string adminEmail); + Task TestConnectivity(string emailUrl, string adminEmail, bool sendEmail); + Task IsDefaultEmailService(); Task SendEmailChangeEmail(ConfirmationEmailDto data); + Task GetVersion(string emailUrl); bool IsValidEmail(string email); - - Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, - bool withHost = true); - - Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider); - Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider); - Task SendKavitaPlusDebug(); } public class EmailService : IEmailService { private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IHostEnvironment _environment; - private readonly ILocalizationService _localizationService; + private readonly IDownloadService _downloadService; - private const string TemplatePath = @"{0}.html"; - private const string LocalHost = "localhost:4200"; + /// + /// This is used to initially set or reset the ServerSettingKey. Do not access from the code, access via UnitOfWork + /// + public const string DefaultApiUrl = "https://email.kavitareader.com"; - public const string SendToDeviceTemplate = "SendToDevice"; - public const string EmailTestTemplate = "EmailTest"; - public const string EmailChangeTemplate = "EmailChange"; - public const string TokenExpirationTemplate = "TokenExpiration"; - public const string TokenExpiringSoonTemplate = "TokenExpiringSoon"; - public const string EmailConfirmTemplate = "EmailConfirm"; - public const string EmailPasswordResetTemplate = "EmailPasswordReset"; - public const string KavitaPlusDebugTemplate = "KavitaPlusDebug"; - - public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, - IHostEnvironment environment, ILocalizationService localizationService) + public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDownloadService downloadService) { _logger = logger; _unitOfWork = unitOfWork; - _directoryService = directoryService; - _environment = environment; - _localizationService = localizationService; + _downloadService = downloadService; + + + FlurlHttp.ConfigureClient(DefaultApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } /// - /// Test if the email settings are working. Rejects if user email isn't valid or not all data is setup in server settings. + /// Test if this instance is accessible outside the network /// + /// This will do some basic filtering to auto return false if the emailUrl is a LAN ip + /// + /// Should an email be sent if connectivity is successful /// - public async Task SendTestEmail(string adminEmail) + public async Task TestConnectivity(string emailUrl, string adminEmail, bool sendEmail) { - var result = new EmailTestResultDto - { - EmailAddress = adminEmail - }; - - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!IsValidEmail(adminEmail)) - { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "account-email-invalid"); - result.Successful = false; - return result; - } - - if (!settings.IsEmailSetup()) - { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "email-settings-invalid"); - result.Successful = false; - return result; - } - - var placeholders = new List> - { - new ("{{Host}}", settings.HostName), - }; - + var result = new EmailTestResultDto(); try { - var emailOptions = new EmailOptionsDto() + if (IsLocalIpAddress(emailUrl)) { - Subject = "Kavita - Email Test", - Template = EmailTestTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(EmailTestTemplate), placeholders), - Preheader = "Kavita - Email Test", - ToEmails = new List() - { - adminEmail - }, - }; - - await SendEmail(emailOptions); - result.Successful = true; + result.Successful = false; + result.ErrorMessage = "This is a local IP address"; + } + result.Successful = await SendEmailWithGet($"{emailUrl}/api/test?adminEmail={Url.Encode(adminEmail)}&sendEmail={sendEmail}"); } catch (KavitaException ex) { @@ -146,384 +81,229 @@ public class EmailService : IEmailService return result; } - /// - /// Sends an email that has a link that will finalize an Email Change - /// - /// + public async Task IsDefaultEmailService() + { + return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value! + .Equals(DefaultApiUrl); + } + public async Task SendEmailChangeEmail(ConfirmationEmailDto data) { - var placeholders = new List> + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value; + var success = await SendEmailWithPost(emailLink + "/api/account/email-change", data); + if (!success) { - new ("{{InvitingUser}}", data.InvitingUser), - new ("{{Link}}", data.ServerConfirmationLink) - }; - - var emailOptions = new EmailOptionsDto() - { - Subject = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders), - Template = EmailChangeTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(EmailChangeTemplate), placeholders), - Preheader = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders), - ToEmails = new List() - { - data.EmailAddress - } - }; - - await SendEmail(emailOptions); + _logger.LogError("There was a critical error sending Confirmation email"); + } } - /// - /// Validates the email address. Does not test it actually receives mail - /// - /// - /// - public bool IsValidEmail(string? email) + public async Task GetVersion(string emailUrl) { - return !string.IsNullOrEmpty(email) && new EmailAddressAttribute().IsValid(email); - } - - public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) - { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); - var basePart = $"{request.Scheme}://{host}{request.PathBase}"; - if (!string.IsNullOrEmpty(serverSettings.HostName)) + try { - basePart = serverSettings.HostName; - if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var response = await $"{emailUrl}/api/about/version" + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("x-kavita-installId", settings.InstallId) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(10)) + .GetStringAsync(); + + if (!string.IsNullOrEmpty(response)) { - var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0; - basePart += serverSettings.BaseUrl[..^removeCount]; + return response.Replace("\"", string.Empty); } } + catch (Exception) + { + return null; + } - if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; - return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}" - .Replace("//", "/"); + return null; } - public async Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider) + public bool IsValidEmail(string email) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; + return new EmailAddressAttribute().IsValid(email); + } - var placeholders = new List> + public async Task SendConfirmationEmail(ConfirmationEmailDto data) + { + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + var success = await SendEmailWithPost(emailLink + "/api/invite/confirm", data); + if (!success) { - new ("{{UserName}}", user.UserName!), - new ("{{Provider}}", provider.ToDescription()), - new ("{{Link}}", $"{settings.HostName}/settings#account" ), - }; + _logger.LogError("There was a critical error sending Confirmation email"); + } + } - var emailOptions = new EmailOptionsDto() + public async Task CheckIfAccessible(string host) + { + // This is the only exception for using the default because we need an external service to check if the server is accessible for emails + try { - Subject = UpdatePlaceHolders("Kavita - Your {{Provider}} token has expired and scrobbling events have stopped", placeholders), - Template = TokenExpirationTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(TokenExpirationTemplate), placeholders), - Preheader = UpdatePlaceHolders("Kavita - Your {{Provider}} token has expired and scrobbling events have stopped", placeholders), - ToEmails = new List() + if (IsLocalIpAddress(host)) { - user.Email + _logger.LogDebug("[EmailService] Server is not accessible, using local ip"); + return false; } - }; - await SendEmail(emailOptions); - - return true; + var url = DefaultApiUrl + "/api/reachable?host=" + host; + _logger.LogDebug("[EmailService] Checking if this server is accessible for sending an email to: {Url}", url); + return await SendEmailWithGet(url); + } + catch (Exception) + { + return false; + } } - public async Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider) + public async Task SendMigrationEmail(EmailMigrationDto data) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; - - var placeholders = new List> - { - new ("{{UserName}}", user.UserName!), - new ("{{Provider}}", provider.ToDescription()), - new ("{{Link}}", $"{settings.HostName}/settings#account" ), - }; - - var emailOptions = new EmailOptionsDto() - { - Subject = UpdatePlaceHolders("Kavita - Your {{Provider}} token will expire soon!", placeholders), - Template = TokenExpiringSoonTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(TokenExpiringSoonTemplate), placeholders), - Preheader = UpdatePlaceHolders("Kavita - Your {{Provider}} token will expire soon!", placeholders), - ToEmails = new List() - { - user.Email - } - }; - - await SendEmail(emailOptions); - - return true; + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + return await SendEmailWithPost(emailLink + "/api/invite/email-migration", data); } - /// - /// Sends information about Kavita install for Kavita+ registration - /// - /// Users in China can have issues subscribing, this flow will allow me to register their instance on their behalf - /// - public async Task SendKavitaPlusDebug() + public async Task SendPasswordResetEmail(PasswordResetEmailDto data) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!settings.IsEmailSetup()) return false; - - var placeholders = new List> - { - new ("{{InstallId}}", HashUtil.ServerToken()), - new ("{{Build}}", BuildInfo.Version.ToString()), - }; - - var emailOptions = new EmailOptionsDto() - { - Subject = UpdatePlaceHolders("Kavita+: A User needs manual registration", placeholders), - Template = KavitaPlusDebugTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(KavitaPlusDebugTemplate), placeholders), - Preheader = UpdatePlaceHolders("Kavita+: A User needs manual registration", placeholders), - ToEmails = - [ - // My kavita email - Encoding.UTF8.GetString(Convert.FromBase64String("a2F2aXRhcmVhZGVyQGdtYWlsLmNvbQ==")) - ] - }; - - await SendEmail(emailOptions); - - return true; - } - - /// - /// Sends an invite email to a user to setup their account - /// - /// - public async Task SendInviteEmail(ConfirmationEmailDto data) - { - var placeholders = new List> - { - new ("{{InvitingUser}}", data.InvitingUser), - new ("{{Link}}", data.ServerConfirmationLink) - }; - - var emailOptions = new EmailOptionsDto() - { - Subject = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders), - Template = EmailConfirmTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(EmailConfirmTemplate), placeholders), - Preheader = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders), - ToEmails = new List() - { - data.EmailAddress - } - }; - - await SendEmail(emailOptions); - } - - public Task CheckIfAccessible(string host) - { - return Task.FromResult(true); - } - - public async Task SendForgotPasswordEmail(PasswordResetEmailDto dto) - { - var placeholders = new List> - { - new ("{{Link}}", dto.ServerConfirmationLink), - }; - - var emailOptions = new EmailOptionsDto() - { - Subject = UpdatePlaceHolders("A password reset has been requested", placeholders), - Template = EmailPasswordResetTemplate, - Body = UpdatePlaceHolders(await GetEmailBody(EmailPasswordResetTemplate), placeholders), - Preheader = "Email confirmation is required for continued access. Click the button to confirm your email.", - ToEmails = - [ - dto.EmailAddress - ] - }; - - await SendEmail(emailOptions); - return true; + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + return await SendEmailWithPost(emailLink + "/api/invite/email-password-reset", data); } public async Task SendFilesToEmail(SendToDto data) { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!serverSetting.IsEmailSetupForSendToDevice()) return false; - - var emailOptions = new EmailOptionsDto() - { - Subject = "Send file from Kavita", - Preheader = "File(s) sent from Kavita", - ToEmails = [data.DestinationEmail], - Template = SendToDeviceTemplate, - Body = await GetEmailBody(SendToDeviceTemplate), - Attachments = data.FilePaths.ToList() - }; - - await SendEmail(emailOptions); - return true; + if (await IsDefaultEmailService()) return false; + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + return await SendEmailWithFiles(emailLink + "/api/sendto", data.FilePaths, data.DestinationEmail); } - private async Task SendEmail(EmailOptionsDto userEmailOptions) + private async Task SendEmailWithGet(string url, int timeoutSecs = 30) { - var smtpConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; - var email = new MimeMessage() - { - Subject = userEmailOptions.Subject, - }; - email.From.Add(new MailboxAddress(smtpConfig.SenderDisplayName, smtpConfig.SenderAddress)); - - // Inject the body into the base template - var fullBody = UpdatePlaceHolders(await GetEmailBody("base"), new List>() - { - new ("{{Body}}", userEmailOptions.Body), - new ("{{Preheader}}", userEmailOptions.Preheader), - }); - - var body = new BodyBuilder - { - HtmlBody = fullBody - }; - - if (userEmailOptions.Attachments != null) - { - foreach (var attachmentPath in userEmailOptions.Attachments) - { - var mimeType = MimeTypeMap.GetMimeType(attachmentPath) ?? "application/octet-stream"; - var mediaType = mimeType.Split('/')[0]; - var mediaSubtype = mimeType.Split('/')[1]; - - var attachment = new MimePart(mediaType, mediaSubtype) - { - Content = new MimeContent(File.OpenRead(attachmentPath)), - ContentDisposition = new ContentDisposition(ContentDisposition.Attachment), - ContentTransferEncoding = ContentEncoding.Base64, - FileName = Path.GetFileName(attachmentPath) - }; - - body.Attachments.Add(attachment); - } - } - - email.Body = body.ToMessageBody(); - - foreach (var toEmail in userEmailOptions.ToEmails) - { - email.To.Add(new MailboxAddress(toEmail, toEmail)); - } - - using var smtpClient = new MailKit.Net.Smtp.SmtpClient(); - smtpClient.Timeout = 20000; - var ssl = smtpConfig.EnableSsl ? SecureSocketOptions.Auto : SecureSocketOptions.None; - - await smtpClient.ConnectAsync(smtpConfig.Host, smtpConfig.Port, ssl); - if (!string.IsNullOrEmpty(smtpConfig.UserName) && !string.IsNullOrEmpty(smtpConfig.Password)) - { - await smtpClient.AuthenticateAsync(smtpConfig.UserName, smtpConfig.Password); - } - - ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault; - - var emailAddress = userEmailOptions.ToEmails[0]; - AppUser? user; - if (userEmailOptions.Template == SendToDeviceTemplate) - { - user = await _unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress); - } - else - { - user = await _unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress); - } - - try { - await smtpClient.SendAsync(email); - if (user != null) + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var response = await (url) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("x-kavita-installId", settings.InstallId) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(timeoutSecs)) + .GetStringAsync(); + + if (!string.IsNullOrEmpty(response) && bool.Parse(response)) { - await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Sent"); + return true; } } catch (Exception ex) { - _logger.LogError(ex, "There was an issue sending the email"); + throw new KavitaException(ex.Message); + } + return false; + } - if (user != null) + + private async Task SendEmailWithPost(string url, object data, int timeoutSecs = 30) + { + try + { + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var response = await (url) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("x-kavita-installId", settings.InstallId) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(timeoutSecs)) + .PostJsonAsync(data); + + if (response.StatusCode != StatusCodes.Status200OK) { - await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Failed", ex.Message); - } - _logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template); - - throw; - } - finally - { - await smtpClient.DisconnectAsync(true); - - } - } - - /// - /// Logs email history for the specified user. - /// - private async Task LogEmailHistory(int appUserId, string emailTemplate, string subject, string body, string deliveryStatus, string? errorMessage = null) - { - var emailHistory = new EmailHistory - { - AppUserId = appUserId, - EmailTemplate = emailTemplate, - Sent = deliveryStatus == "Sent", - Body = body, - Subject = subject, - SendDate = DateTime.UtcNow, - DeliveryStatus = deliveryStatus, - ErrorMessage = errorMessage - }; - - _unitOfWork.DataContext.EmailHistory.Add(emailHistory); - await _unitOfWork.CommitAsync(); - } - - private async Task GetTemplatePath(string templateName) - { - if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) - { - var templateDirectory = Path.Join(_directoryService.CustomizedTemplateDirectory, TemplatePath); - var fullName = string.Format(templateDirectory, templateName); - if (_directoryService.FileSystem.File.Exists(fullName)) return fullName; - _logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName); - } - - return string.Format(Path.Join(_directoryService.TemplateDirectory, TemplatePath), templateName); - } - - private async Task GetEmailBody(string templateName) - { - var templatePath = await GetTemplatePath(templateName); - - var body = await File.ReadAllTextAsync(templatePath); - return body; - } - - private static string UpdatePlaceHolders(string text, IList>? keyValuePairs) - { - if (string.IsNullOrEmpty(text) || keyValuePairs == null) return text; - - foreach (var (key, value) in keyValuePairs) - { - if (text.Contains(key)) - { - text = text.Replace(key, value); + var errorMessage = await response.GetStringAsync(); + throw new KavitaException(errorMessage); } } - - return text; + catch (FlurlHttpException ex) + { + _logger.LogError(ex, "There was an exception when interacting with Email Service"); + return false; + } + return true; } + + + private async Task SendEmailWithFiles(string url, IEnumerable filePaths, string destEmail, int timeoutSecs = 300) + { + try + { + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var response = await (url) + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("x-kavita-installId", settings.InstallId) + .WithTimeout(timeoutSecs) + .AllowHttpStatus("4xx") + .PostMultipartAsync(mp => + { + mp.AddString("email", destEmail); + var index = 1; + foreach (var filepath in filePaths) + { + mp.AddFile("file" + index, filepath, _downloadService.GetContentTypeFromFile(filepath)); + index++; + } + } + ); + + if (response.StatusCode != StatusCodes.Status200OK) + { + var errorMessage = await response.GetStringAsync(); + throw new KavitaException(errorMessage); + } + } + catch (FlurlHttpException ex) + { + _logger.LogError(ex, "There was an exception when sending Email for SendTo"); + return false; + } + return true; + } + + private static bool IsLocalIpAddress(string url) + { + var host = url.Split(':')[0]; + try + { + // get host IP addresses + var hostIPs = Dns.GetHostAddresses(host); + // get local IP addresses + var localIPs = Dns.GetHostAddresses(Dns.GetHostName()); + + // test if any host IP equals to any local IP or to localhost + foreach (var hostIp in hostIPs) + { + // is localhost + if (IPAddress.IsLoopback(hostIp)) return true; + // is local address + if (localIPs.Contains(hostIp)) + { + return true; + } + } + } + catch + { + // ignored + } + + return false; + } + } diff --git a/API/Services/FileService.cs b/API/Services/FileService.cs index 2cb34c601..a4194b820 100644 --- a/API/Services/FileService.cs +++ b/API/Services/FileService.cs @@ -1,10 +1,5 @@ using System; -using System.IO; using System.IO.Abstractions; -using System.Runtime.Intrinsics.Arm; -using System.Security.Cryptography; -using System.Text; -using System.Text.Unicode; using API.Extensions; namespace API.Services; @@ -14,7 +9,6 @@ public interface IFileService IFileSystem GetFileSystem(); bool HasFileBeenModifiedSince(string filePath, DateTime time); bool Exists(string filePath); - bool ValidateSha(string filepath, string sha); } public class FileService : IFileService @@ -49,27 +43,4 @@ public class FileService : IFileService { return _fileSystem.File.Exists(filePath); } - - /// - /// Validates the Sha256 hash matches - /// - /// - /// - /// - public bool ValidateSha(string filepath, string sha) - { - if (!Exists(filepath)) return false; - if (string.IsNullOrEmpty(sha)) throw new ArgumentException("Sha cannot be null"); - - using var fs = _fileSystem.File.OpenRead(filepath); - fs.Position = 0; - - using var reader = new StreamReader(fs, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); - var content = reader.ReadToEnd(); - - // Compute SHA hash - var checksum = SHA256.HashData(Encoding.UTF8.GetBytes(content)); - - return Convert.ToHexString(checksum).Equals(sha); - } } diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/API/Services/HostedServices/StartupTasksHostedService.cs index 145fb8e2b..d7d74f77d 100644 --- a/API/Services/HostedServices/StartupTasksHostedService.cs +++ b/API/Services/HostedServices/StartupTasksHostedService.cs @@ -3,12 +3,10 @@ using System.Threading; using System.Threading.Tasks; using API.Data; using API.Services.Tasks.Scanner; -using Hangfire; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace API.Services.HostedServices; -#nullable enable public class StartupTasksHostedService : IHostedService { @@ -46,8 +44,7 @@ public class StartupTasksHostedService : IHostedService if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching) { var libraryWatcher = scope.ServiceProvider.GetRequiredService(); - // Push this off for a bit for people with massive libraries, as it can take up to 45 mins and blocks the thread - BackgroundJob.Enqueue(() => libraryWatcher.StartWatching()); + await libraryWatcher.StartWatching(); } } catch (Exception) diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 544efa4ce..062b88935 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -2,17 +2,17 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Numerics; using System.Threading.Tasks; -using API.DTOs; +using API.Constants; using API.Entities.Enums; -using API.Entities.Interfaces; using API.Extensions; +using EasyCaching.Core; +using Flurl; +using Flurl.Http; +using HtmlAgilityPack; +using Kavita.Common; using Microsoft.Extensions.Logging; using NetVips; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Quantization; using Image = NetVips.Image; namespace API.Services; @@ -50,7 +50,6 @@ public interface IImageService /// /// string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - /// /// Converts the passed image to encoding and outputs it in the same directory /// @@ -60,23 +59,20 @@ public interface IImageService /// File of written encoded image Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat); Task IsImage(string filePath); - void UpdateColorScape(IHasCoverImage entity); + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); } public class ImageService : IImageService { - public const string Name = "ImageService"; + public const string Name = "BookmarkService"; private readonly ILogger _logger; private readonly IDirectoryService _directoryService; - + private readonly IEasyCachingProviderFactory _cacheFactory; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+"; public const string ReadingListCoverImageRegex = @"readinglist\d+"; - private const double WhiteThreshold = 0.95; // Colors with lightness above this are considered too close to white - private const double BlackThreshold = 0.25; // Colors with lightness below this are considered too close to black - /// /// Width of the Thumbnail generation @@ -92,10 +88,26 @@ public class ImageService : IImageService public const int LibraryThumbnailWidth = 32; - public ImageService(ILogger logger, IDirectoryService directoryService) + private static readonly string[] ValidIconRelations = { + "icon", + "apple-touch-icon", + "apple-touch-icon-precomposed", + "apple-touch-icon icon-precomposed" // ComicVine has it combined + }; + + /// + /// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon) + /// + private static readonly IDictionary FaviconUrlMapper = new Dictionary + { + ["https://app.plex.tv"] = "https://plex.tv" + }; + + public ImageService(ILogger logger, IDirectoryService directoryService, IEasyCachingProviderFactory cacheFactory) { _logger = logger; _directoryService = directoryService; + _cacheFactory = cacheFactory; } public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) @@ -113,90 +125,14 @@ public class ImageService : IImageService } } - /// - /// Tries to determine if there is a better mode for resizing - /// - /// - /// - /// - /// - public static Enums.Size GetSizeForDimensions(Image image, int targetWidth, int targetHeight) - { - try - { - if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) - { - return Enums.Size.Force; - } - } - catch (Exception) - { - /* Swallow */ - } - - return Enums.Size.Both; - } - - public static Enums.Interesting? GetCropForDimensions(Image image, int targetWidth, int targetHeight) - { - try - { - if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height)) - { - return null; - } - } catch (Exception) - { - /* Swallow */ - return null; - } - - return Enums.Interesting.Attention; - } - - public static bool WillScaleWell(Image sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1) - { - // Calculate the aspect ratios - var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height; - var targetAspectRatio = (double) targetWidth / targetHeight; - - // Compare aspect ratios - if (Math.Abs(sourceAspectRatio - targetAspectRatio) > tolerance) - { - return false; // Aspect ratios differ significantly - } - - // Calculate scaling factors - var widthScaleFactor = (double) targetWidth / sourceImage.Width; - var heightScaleFactor = (double) targetHeight / sourceImage.Height; - - // Check resolution quality (example thresholds) - if (widthScaleFactor > 2.0 || heightScaleFactor > 2.0) - { - return false; // Scaling factor too large - } - - return true; // Image will scale well - } - - private static bool IsLikelyWideImage(int width, int height) - { - var aspectRatio = (double) width / height; - return aspectRatio > 1.25; - } - public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size) { if (string.IsNullOrEmpty(path)) return string.Empty; try { - var (width, height) = size.GetDimensions(); - using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered); - - using var thumbnail = Image.Thumbnail(path, width, height: height, - size: GetSizeForDimensions(sourceImage, width, height), - crop: GetCropForDimensions(sourceImage, width, height)); + var dims = size.GetDimensions(); + using var thumbnail = Image.Thumbnail(path, dims.Width, height: dims.Height, size: Enums.Size.Force); var filename = fileName + encodeFormat.GetExtension(); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; @@ -211,7 +147,7 @@ public class ImageService : IImageService /// /// Creates a thumbnail out of a memory stream and saves to with the passed - /// fileName and the appropriate extension. + /// fileName and .png extension. /// /// Stream to write to disk. Ensure this is rewinded. /// filename to save as without extension @@ -220,54 +156,22 @@ public class ImageService : IImageService /// File name with extension of the file. This will always write to public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - var (targetWidth, targetHeight) = size.GetDimensions(); - if (stream.CanSeek) stream.Position = 0; - using var sourceImage = Image.NewFromStream(stream); - - var scalingSize = GetSizeForDimensions(sourceImage, targetWidth, targetHeight); - var scalingCrop = GetCropForDimensions(sourceImage, targetWidth, targetHeight); - - using var thumbnail = sourceImage.ThumbnailImage(targetWidth, targetHeight, - size: scalingSize, - crop: scalingCrop); - + var dims = size.GetDimensions(); + using var thumbnail = Image.ThumbnailStream(stream, dims.Width, height: dims.Height, size: Enums.Size.Force); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); - try { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - - try - { - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - - return filename; - } - catch (VipsException) - { - // NetVips Issue: https://github.com/kleisauke/net-vips/issues/234 - // Saving pdf covers from a stream can fail, so revert to old code - - if (stream.CanSeek) stream.Position = 0; - using var thumbnail2 = Image.ThumbnailStream(stream, targetWidth, height: targetHeight, - size: scalingSize, - crop: scalingCrop); - thumbnail2.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - - return filename; - } + thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + return filename; } public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) { - var (width, height) = size.GetDimensions(); - using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered); - - using var thumbnail = Image.Thumbnail(sourceFile, width, height: height, - size: GetSizeForDimensions(sourceImage, width, height), - crop: GetCropForDimensions(sourceImage, width, height)); + var dims = size.GetDimensions(); + using var thumbnail = Image.Thumbnail(sourceFile, dims.Width, height: dims.Height, size: Enums.Size.Force); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); try @@ -311,274 +215,132 @@ public class ImageService : IImageService return false; } - - - private static (Vector3?, Vector3?) GetPrimarySecondaryColors(string imagePath) + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) { - using var image = Image.NewFromFile(imagePath); - // Resize the image to speed up processing - var resizedImage = image.Resize(0.1); - - var processedImage = PreProcessImage(resizedImage); + // Parse the URL to get the domain (including subdomain) + var uri = new Uri(url); + var domain = uri.Host; + var baseUrl = uri.Scheme + "://" + uri.Host; - // Convert image to RGB array - var pixels = processedImage.WriteToMemory().ToArray(); - - // Convert to list of Vector3 (RGB) - var rgbPixels = new List(); - for (var i = 0; i < pixels.Length - 2; i += 3) + var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); + var res = await provider.GetAsync(baseUrl); + if (res.HasValue) { - rgbPixels.Add(new Vector3(pixels[i], pixels[i + 1], pixels[i + 2])); + _logger.LogInformation("Kavita has already tried to fetch from {BaseUrl} and failed. Skipping duplicate check", baseUrl); + throw new KavitaException($"Kavita has already tried to fetch from {baseUrl} and failed. Skipping duplicate check"); } - // Perform k-means clustering - var clusters = KMeansClustering(rgbPixels, 4); - - var sorted = SortByVibrancy(clusters); - - // Ensure white and black are not selected as primary/secondary colors - sorted = sorted.Where(c => !IsCloseToWhiteOrBlack(c)).ToList(); - - if (sorted.Count >= 2) + await provider.SetAsync(baseUrl, string.Empty, TimeSpan.FromDays(10)); + if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) { - return (sorted[0], sorted[1]); - } - if (sorted.Count == 1) - { - return (sorted[0], null); + url = value; } - return (null, null); - } + var correctSizeLink = string.Empty; - private static (Vector3?, Vector3?) GetPrimaryColorSharp(string imagePath) - { - using var image = SixLabors.ImageSharp.Image.Load(imagePath); - - image.Mutate( - x => x - // Scale the image down preserving the aspect ratio. This will speed up quantization. - // We use nearest neighbor as it will be the fastest approach. - .Resize(new ResizeOptions() { Sampler = KnownResamplers.NearestNeighbor, Size = new SixLabors.ImageSharp.Size(100, 0) }) - - // Reduce the color palette to 1 color without dithering. - .Quantize(new OctreeQuantizer(new QuantizerOptions { MaxColors = 4 }))); - - Rgb24 dominantColor = image[0, 0]; - - // This will give you a dominant color in HEX format i.e #5E35B1FF - return (new Vector3(dominantColor.R, dominantColor.G, dominantColor.B), new Vector3(dominantColor.R, dominantColor.G, dominantColor.B)); - } - - private static Image PreProcessImage(Image image) - { - return image; - // Create a mask for white and black pixels - var whiteMask = image.Colourspace(Enums.Interpretation.Lab)[0] > (WhiteThreshold * 100); - var blackMask = image.Colourspace(Enums.Interpretation.Lab)[0] < (BlackThreshold * 100); - - // Create a replacement color (e.g., medium gray) - var replacementColor = new[] { 240.0, 240.0, 240.0 }; - - // Apply the masks to replace white and black pixels - var processedImage = image.Copy(); - processedImage = processedImage.Ifthenelse(whiteMask, replacementColor); - //processedImage = processedImage.Ifthenelse(blackMask, replacementColor); - - return processedImage; - } - - private static Dictionary GenerateColorHistogram(Image image) - { - var pixels = image.WriteToMemory().ToArray(); - var histogram = new Dictionary(); - - for (var i = 0; i < pixels.Length; i += 3) + try { - var color = new Vector3(pixels[i], pixels[i + 1], pixels[i + 2]); - if (!histogram.TryAdd(color, 1)) + var htmlContent = url.GetStringAsync().Result; + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(htmlContent); + var pngLinks = htmlDocument.DocumentNode.Descendants("link") + .Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty))) + .Select(link => link.GetAttributeValue("href", string.Empty)) + .Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + correctSizeLink = (pngLinks?.Find(pngLink => pngLink.Contains("32")) ?? pngLinks?.FirstOrDefault()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading favicon.png for {Domain}, will try fallback methods", domain); + } + + try + { + if (string.IsNullOrEmpty(correctSizeLink)) { - histogram[color]++; + correctSizeLink = FallbackToKavitaReaderFavicon(baseUrl); } - } - - return histogram; - } - - private static bool IsColorCloseToWhiteOrBlack(Vector3 color) - { - var (_, _, lightness) = RgbToHsl(color); - return lightness is > WhiteThreshold or < BlackThreshold; - } - - private static List KMeansClustering(List points, int k, int maxIterations = 100) - { - var random = new Random(); - var centroids = points.OrderBy(x => random.Next()).Take(k).ToList(); - - for (var i = 0; i < maxIterations; i++) - { - var clusters = new List[k]; - for (var j = 0; j < k; j++) + if (string.IsNullOrEmpty(correctSizeLink)) { - clusters[j] = []; + throw new KavitaException($"Could not grab favicon from {baseUrl}"); } - foreach (var point in points) + var finalUrl = correctSizeLink; + + // If starts with //, it's coming usually from an offsite cdn + if (correctSizeLink.StartsWith("//")) { - var nearestCentroidIndex = centroids - .Select((centroid, index) => new { Index = index, Distance = Vector3.DistanceSquared(centroid, point) }) - .OrderBy(x => x.Distance) - .First().Index; - clusters[nearestCentroidIndex].Add(point); + finalUrl = "https:" + correctSizeLink; + } + else if (!correctSizeLink.StartsWith(uri.Scheme)) + { + finalUrl = Url.Combine(baseUrl, correctSizeLink); } - var newCentroids = clusters.Select(cluster => - cluster.Count != 0 ? new Vector3( - cluster.Average(p => p.X), - cluster.Average(p => p.Y), - cluster.Average(p => p.Z) - ) : Vector3.Zero - ).ToList(); + _logger.LogTrace("Fetching favicon from {Url}", finalUrl); + // Download the favicon.ico file using Flurl + var faviconStream = await finalUrl + .AllowHttpStatus("2xx,304") + .GetStreamAsync(); - if (centroids.SequenceEqual(newCentroids)) - break; + // Create the destination file path + using var image = Image.PngloadStream(faviconStream); + var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); + switch (encodeFormat) + { + case EncodeFormat.PNG: + image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + case EncodeFormat.WEBP: + image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + case EncodeFormat.AVIF: + image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); + } - centroids = newCentroids; + + _logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain); + return filename; + }catch (Exception ex) + { + _logger.LogError(ex, "Error downloading favicon.png for {Domain}", domain); + throw; + } + } + + private static string FallbackToKavitaReaderFavicon(string baseUrl) + { + var correctSizeLink = string.Empty; + var allOverrides = "https://kavitareader.com/assets/favicons/urls.txt".GetStringAsync().Result; + if (!string.IsNullOrEmpty(allOverrides)) + { + var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); + var externalFile = allOverrides + .Split("\n") + .FirstOrDefault(url => + cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || + cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) + )); + if (string.IsNullOrEmpty(externalFile)) + { + throw new KavitaException($"Could not grab favicon from {baseUrl}"); + } + + correctSizeLink = "https://kavitareader.com/assets/favicons/" + externalFile; } - return centroids; + return correctSizeLink; } - public static List SortByBrightness(List colors) - { - return colors.OrderBy(c => 0.299 * c.X + 0.587 * c.Y + 0.114 * c.Z).ToList(); - } - - private static List SortByVibrancy(List colors) - { - return colors.OrderByDescending(c => - { - var max = Math.Max(c.X, Math.Max(c.Y, c.Z)); - var min = Math.Min(c.X, Math.Min(c.Y, c.Z)); - return (max - min) / max; - }).ToList(); - } - - private static bool IsCloseToWhiteOrBlack(Vector3 color) - { - var threshold = 30; - return (color.X > 255 - threshold && color.Y > 255 - threshold && color.Z > 255 - threshold) || - (color.X < threshold && color.Y < threshold && color.Z < threshold); - } - - private static string RgbToHex(Vector3 color) - { - return $"#{(int)color.X:X2}{(int)color.Y:X2}{(int)color.Z:X2}"; - } - - private static Vector3 GetComplementaryColor(Vector3 color) - { - // Convert RGB to HSL - var (h, s, l) = RgbToHsl(color); - - // Rotate hue by 180 degrees - h = (h + 180) % 360; - - // Convert back to RGB - return HslToRgb(h, s, l); - } - - private static (double H, double S, double L) RgbToHsl(Vector3 rgb) - { - double r = rgb.X / 255; - double g = rgb.Y / 255; - double b = rgb.Z / 255; - - var max = Math.Max(r, Math.Max(g, b)); - var min = Math.Min(r, Math.Min(g, b)); - var diff = max - min; - - double h = 0; - double s = 0; - var l = (max + min) / 2; - - if (Math.Abs(diff) > 0.00001) - { - s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); - - if (max == r) - h = (g - b) / diff + (g < b ? 6 : 0); - else if (max == g) - h = (b - r) / diff + 2; - else if (max == b) - h = (r - g) / diff + 4; - - h *= 60; - } - - return (h, s, l); - } - - private static Vector3 HslToRgb(double h, double s, double l) - { - double r, g, b; - - if (Math.Abs(s) < 0.00001) - { - r = g = b = l; - } - else - { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = HueToRgb(p, q, h + 120); - g = HueToRgb(p, q, h); - b = HueToRgb(p, q, h - 120); - } - - return new Vector3((float)(r * 255), (float)(g * 255), (float)(b * 255)); - } - - private static double HueToRgb(double p, double q, double t) - { - if (t < 0) t += 360; - if (t > 360) t -= 360; - return t switch - { - < 60 => p + (q - p) * t / 60, - < 180 => q, - < 240 => p + (q - p) * (240 - t) / 60, - _ => p - }; - } - - /// - /// Generates the Primary and Secondary colors from a file - /// - /// This may use a second most common color or a complementary color. It's up to implemenation to choose what's best - /// - /// - public static ColorScape CalculateColorScape(string sourceFile) - { - if (!File.Exists(sourceFile)) return new ColorScape() {Primary = null, Secondary = null}; - - var colors = GetPrimarySecondaryColors(sourceFile); - - return new ColorScape() - { - Primary = colors.Item1 == null ? null : RgbToHex(colors.Item1.Value), - Secondary = colors.Item2 == null ? null : RgbToHex(colors.Item2.Value) - }; - } - - - /// public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) { - // TODO: This code has no concept of cropping nor Thumbnail Size try { using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); @@ -594,7 +356,6 @@ public class ImageService : IImageService return string.Empty; } - /// /// Returns the name format for a chapter cover image /// @@ -606,16 +367,6 @@ public class ImageService : IImageService return $"v{volumeId}_c{chapterId}"; } - /// - /// Returns the name format for a volume cover image (custom) - /// - /// - /// - public static string GetVolumeFormat(int volumeId) - { - return $"v{volumeId}"; - } - /// /// Returns the name format for a library cover image /// @@ -667,30 +418,15 @@ public class ImageService : IImageService return $"thumbnail{chapterId}"; } - /// - /// Returns the name format for a person cover - /// - /// - /// - public static string GetPersonFormat(int personId) - { - return $"person{personId}"; - } - public static string GetWebLinkFormat(string url, EncodeFormat encodeFormat) { return $"{new Uri(url).Host.Replace("www.", string.Empty)}{encodeFormat.GetExtension()}"; } - public static string GetPublisherFormat(string publisher, EncodeFormat encodeFormat) - { - return $"{publisher}{encodeFormat.GetExtension()}"; - } - public static void CreateMergedImage(IList coverImages, CoverImageSize size, string dest) { - var (width, height) = size.GetDimensions(); + var dims = size.GetDimensions(); int rows, cols; if (coverImages.Count == 1) @@ -703,21 +439,25 @@ public class ImageService : IImageService rows = 1; cols = 2; } - else + else if (coverImages.Count == 3) { rows = 2; cols = 2; } + else + { + // Default to 2x2 layout for more than 3 images + rows = 2; + cols = 2; + } - - var image = Image.Black(width, height); + var image = Image.Black(dims.Width, dims.Height); var thumbnailWidth = image.Width / cols; var thumbnailHeight = image.Height / rows; for (var i = 0; i < coverImages.Count; i++) { - if (!File.Exists(coverImages[i])) continue; var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential); tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight); @@ -738,42 +478,4 @@ public class ImageService : IImageService image.WriteToFile(dest); } - - public void UpdateColorScape(IHasCoverImage entity) - { - var colors = CalculateColorScape( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); - entity.PrimaryColor = colors.Primary; - entity.SecondaryColor = colors.Secondary; - } - - - public static (int R, int G, int B) HexToRgb(string? hex) - { - if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null"); - - // Remove the leading '#' if present - hex = hex.TrimStart('#'); - - // Ensure the hex string is valid - if (hex.Length != 6 && hex.Length != 3) - { - throw new ArgumentException("Hex string should be 6 or 3 characters long."); - } - - if (hex.Length == 3) - { - // Expand shorthand notation to full form (e.g., "abc" -> "aabbcc") - hex = string.Concat(hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]); - } - - // Parse the hex string into RGB components - var r = Convert.ToInt32(hex.Substring(0, 2), 16); - var g = Convert.ToInt32(hex.Substring(2, 2), 16); - var b = Convert.ToInt32(hex.Substring(4, 2), 16); - - return (r, g, b); - } - - } diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs deleted file mode 100644 index a38e8c468..000000000 --- a/API/Services/KoreaderService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IKoreaderService -{ - Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId); - Task GetProgress(string bookHash, int userId); -} - -public class KoreaderService : IKoreaderService -{ - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger logger) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _logger = logger; - } - - /// - /// Given a Koreader hash, locate the underlying file and generate/update a progress event. - /// - /// - /// - public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) - { - _logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.Progress.Sanitize()); - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - if (userProgressDto == null) - { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId); - if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - - var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); - if (volumeDto == null) throw new KavitaException(await _localizationService.Translate(userId, "volume-doesnt-exist")); - - userProgressDto = new ProgressDto() - { - ChapterId = file.ChapterId, - VolumeId = chapterDto.VolumeId, - SeriesId = volumeDto.SeriesId, - }; - } - // Update the bookScrollId if possible - KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.Progress); - - await _readerService.SaveReadingProgress(userProgressDto, userId); - } - - /// - /// Returns a Koreader Dto representing current book and the progress within - /// - /// - /// - /// - public async Task GetProgress(string bookHash, int userId) - { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash); - - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); - - return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress) - .WithPercentage(progressDto?.PageNum, file.Pages) - .WithDeviceId(settingsDto.InstallId, userId) - .Build(); - } -} diff --git a/API/Services/LocalizationService.cs b/API/Services/LocalizationService.cs index 7db35bb8e..ab3ad3d89 100644 --- a/API/Services/LocalizationService.cs +++ b/API/Services/LocalizationService.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; using API.Data; -using API.DTOs; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; @@ -12,13 +11,11 @@ namespace API.Services; #nullable enable - - public interface ILocalizationService { Task Get(string locale, string key, params object[] args); Task Translate(int userId, string key, params object[] args); - IEnumerable GetLocales(); + IEnumerable GetLocales(); } public class LocalizationService : ILocalizationService @@ -137,260 +134,14 @@ public class LocalizationService : ILocalizationService /// Returns all available locales that exist on both the Frontend and the Backend /// /// - public IEnumerable GetLocales() + public IEnumerable GetLocales() { var uiLanguages = _directoryService - .GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json"); + .GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json") + .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)); var backendLanguages = _directoryService - .GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json"); - - var locales = new Dictionary(); - var localeCounts = new Dictionary>(); // fileName -> (nonEmptyValues, totalKeys) - - // First pass: collect all files and count non-empty strings - - // Process UI language files - foreach (var file in uiLanguages) - { - var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(file); - var fileContent = _directoryService.FileSystem.File.ReadAllText(file); - var hash = ComputeHash(fileContent); - - var counts = CalculateNonEmptyStrings(fileContent); - - if (localeCounts.TryGetValue(fileName, out var existingCount)) - { - // Update existing counts - localeCounts[fileName] = Tuple.Create( - existingCount.Item1 + counts.Item1, - existingCount.Item2 + counts.Item2 - ); - } - else - { - // Add new counts - localeCounts[fileName] = counts; - } - - if (!locales.TryGetValue(fileName, out var locale)) - { - locales[fileName] = new KavitaLocale - { - FileName = fileName, - RenderName = GetDisplayName(fileName), - TranslationCompletion = 0, // Will be calculated later - IsRtL = IsRightToLeft(fileName), - Hash = hash - }; - } - else - { - // Update existing locale hash - locale.Hash = CombineHashes(locale.Hash, hash); - } - } - - // Process backend language files - foreach (var file in backendLanguages) - { - var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(file); - var fileContent = _directoryService.FileSystem.File.ReadAllText(file); - var hash = ComputeHash(fileContent); - - var counts = CalculateNonEmptyStrings(fileContent); - - if (localeCounts.TryGetValue(fileName, out var existingCount)) - { - // Update existing counts - localeCounts[fileName] = Tuple.Create( - existingCount.Item1 + counts.Item1, - existingCount.Item2 + counts.Item2 - ); - } - else - { - // Add new counts - localeCounts[fileName] = counts; - } - - if (!locales.TryGetValue(fileName, out var locale)) - { - locales[fileName] = new KavitaLocale - { - FileName = fileName, - RenderName = GetDisplayName(fileName), - TranslationCompletion = 0, // Will be calculated later - IsRtL = IsRightToLeft(fileName), - Hash = hash - }; - } - else - { - // Update existing locale hash - locale.Hash = CombineHashes(locale.Hash, hash); - } - } - - // Second pass: calculate completion percentages based on English total - if (localeCounts.TryGetValue("en", out var englishCounts) && englishCounts.Item2 > 0) - { - var englishTotalKeys = englishCounts.Item2; - - foreach (var locale in locales.Values) - { - if (localeCounts.TryGetValue(locale.FileName, out var counts)) - { - // Calculate percentage based on English total keys - locale.TranslationCompletion = (float)counts.Item1 / englishTotalKeys * 100; - } - } - } - - return locales.Values; - } - - // Helper methods that would need to be implemented - private static string ComputeHash(string content) - { - // Implement a hashing algorithm (e.g., SHA256, MD5) to generate a hash for the content - using var md5 = System.Security.Cryptography.MD5.Create(); - var inputBytes = System.Text.Encoding.UTF8.GetBytes(content); - var hashBytes = md5.ComputeHash(inputBytes); - return Convert.ToBase64String(hashBytes); - } - - private static string CombineHashes(string hash1, string hash2) - { - // Combine two hashes, possibly by concatenating and rehashing - return ComputeHash(hash1 + hash2); - } - - private static string GetDisplayName(string fileName) - { - // Map the filename to a human-readable display name - // This could use a lookup table or follow a naming convention - try - { - var cultureInfo = new System.Globalization.CultureInfo(fileName.Replace('_', '-')); - return cultureInfo.NativeName; - } - catch - { - // Fall back to the file name if the culture isn't recognized - return fileName; - } - } - - private static bool IsRightToLeft(string fileName) - { - // Determine if the language is right-to-left - try - { - var cultureInfo = new System.Globalization.CultureInfo(fileName); - return cultureInfo.TextInfo.IsRightToLeft; - } - catch - { - return false; // Default to left-to-right - } - } - - private static float CalculateTranslationCompletion(string fileContent) - { - try - { - var jsonObject = System.Text.Json.JsonDocument.Parse(fileContent); - - int totalKeys = 0; - int nonEmptyValues = 0; - - // Count all keys and non-empty values - CountNonEmptyValues(jsonObject.RootElement, ref totalKeys, ref nonEmptyValues); - - return totalKeys > 0 ? (nonEmptyValues * 1f) / totalKeys * 100 : 0; - } - catch (Exception ex) - { - // Consider logging the exception - return 0; // Return 0% completion if there's an error parsing - } - } - private static Tuple CalculateNonEmptyStrings(string fileContent) - { - try - { - var jsonObject = JsonDocument.Parse(fileContent); - - var totalKeys = 0; - var nonEmptyValues = 0; - - // Count all keys and non-empty values - CountNonEmptyValues(jsonObject.RootElement, ref totalKeys, ref nonEmptyValues); - - return Tuple.Create(nonEmptyValues, totalKeys); - } - catch (Exception) - { - // Consider logging the exception - return Tuple.Create(0, 0); // Return 0% completion if there's an error parsing - } - } - - private static void CountNonEmptyValues(JsonElement element, ref int totalKeys, ref int nonEmptyValues) - { - if (element.ValueKind == JsonValueKind.Object) - { - foreach (var property in element.EnumerateObject()) - { - if (property.Value.ValueKind == System.Text.Json.JsonValueKind.String) - { - totalKeys++; - var value = property.Value.GetString(); - if (!string.IsNullOrWhiteSpace(value)) - { - nonEmptyValues++; - } - } - else - { - // Recursively process nested objects - CountNonEmptyValues(property.Value, ref totalKeys, ref nonEmptyValues); - } - } - } - else if (element.ValueKind == System.Text.Json.JsonValueKind.Array) - { - foreach (var item in element.EnumerateArray()) - { - CountNonEmptyValues(item, ref totalKeys, ref nonEmptyValues); - } - } - } - - private void CountEntries(System.Text.Json.JsonElement element, ref int total, ref int translated) - { - if (element.ValueKind == System.Text.Json.JsonValueKind.Object) - { - foreach (var property in element.EnumerateObject()) - { - CountEntries(property.Value, ref total, ref translated); - } - } - else if (element.ValueKind == System.Text.Json.JsonValueKind.Array) - { - foreach (var item in element.EnumerateArray()) - { - CountEntries(item, ref total, ref translated); - } - } - else if (element.ValueKind == System.Text.Json.JsonValueKind.String) - { - total++; - string value = element.GetString(); - if (!string.IsNullOrWhiteSpace(value)) - { - translated++; - } - } + .GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json") + .Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)); + return uiLanguages.Intersect(backendLanguages).Distinct(); } } diff --git a/API/Services/MediaConversionService.cs b/API/Services/MediaConversionService.cs index fc3e5f318..095509676 100644 --- a/API/Services/MediaConversionService.cs +++ b/API/Services/MediaConversionService.cs @@ -197,7 +197,7 @@ public class MediaConversionService : IMediaConversionService foreach (var volume in nonCustomOrConvertedVolumeCovers) { if (string.IsNullOrEmpty(volume.CoverImage)) continue; - volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + volume.CoverImage = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)?.CoverImage; _unitOfWork.VolumeRepository.Update(volume); await _unitOfWork.CommitAsync(); } @@ -222,10 +222,6 @@ public class MediaConversionService : IMediaConversionService { if (string.IsNullOrEmpty(series.CoverImage)) continue; series.CoverImage = series.GetCoverImage(); - if (series.CoverImage == null) - { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); - } _unitOfWork.SeriesRepository.Update(series); await _unitOfWork.CommitAsync(); } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index e0e86f4dc..eba7977e8 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -7,7 +7,6 @@ using API.Comparators; using API.Data; using API.Entities; using API.Entities.Enums; -using API.Entities.Interfaces; using API.Extensions; using API.Helpers; using API.SignalR; @@ -15,7 +14,6 @@ using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable public interface IMetadataService { @@ -26,22 +24,19 @@ public interface IMetadataService /// [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false); + Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false); /// - /// Performs a forced refresh of cover images just for a series, and it's nested entities + /// Performs a forced refresh of cover images just for a series and it's nested entities /// /// /// /// Overrides any cache logic and forces execution - Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true); - Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true); + Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true); + Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false); Task RemoveAbandonedMetadataKeys(); } -/// -/// Handles everything around Cover/ColorScape management -/// public class MetadataService : IMetadataService { public const string Name = "MetadataService"; @@ -51,13 +46,10 @@ public class MetadataService : IMetadataService private readonly ICacheHelper _cacheHelper; private readonly IReadingItemService _readingItemService; private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; private readonly IList _updateEvents = new List(); - public MetadataService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, ICacheHelper cacheHelper, - IReadingItemService readingItemService, IDirectoryService directoryService, - IImageService imageService) + IReadingItemService readingItemService, IDirectoryService directoryService) { _unitOfWork = unitOfWork; _logger = logger; @@ -65,7 +57,6 @@ public class MetadataService : IMetadataService _cacheHelper = cacheHelper; _readingItemService = readingItemService; _directoryService = directoryService; - _imageService = imageService; } /// @@ -74,40 +65,24 @@ public class MetadataService : IMetadataService /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Convert image to Encoding Format when extracting the cover - /// Force colorscape gen - private bool UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) + private Task UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize) { - if (chapter == null) return false; - var firstFile = chapter.Files.MinBy(x => x.Chapter); - if (firstFile == null) return false; + if (firstFile == null) return Task.FromResult(false); - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), + if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) - { - if (NeedsColorSpace(chapter, forceColorScape)) - { - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); - _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); - } + return Task.FromResult(false); - return false; - } _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize); - - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); - _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); - return true; + return Task.FromResult(true); } private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate) @@ -118,60 +93,27 @@ public class MetadataService : IMetadataService firstFile.UpdateLastModified(); } - private static bool NeedsColorSpace(IHasCoverImage? entity, bool force) - { - if (entity == null) return false; - if (force) return true; - - return !string.IsNullOrEmpty(entity.CoverImage) && - (string.IsNullOrEmpty(entity.PrimaryColor) || string.IsNullOrEmpty(entity.SecondaryColor)); - } - - - /// /// Updates the cover image for a Volume /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - /// Force updating colorscape - private bool UpdateVolumeCoverImage(Volume? volume, bool forceUpdate, bool forceColorScape = false) + private Task UpdateVolumeCoverImage(Volume? volume, bool forceUpdate) { // We need to check if Volume coverImage matches first chapters if forceUpdate is false - if (volume == null) return false; - - if (!_cacheHelper.ShouldUpdateCoverImage( + if (volume == null || !_cacheHelper.ShouldUpdateCoverImage( _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), - null, volume.Created, forceUpdate)) - { - if (NeedsColorSpace(volume, forceColorScape)) - { - _imageService.UpdateColorScape(volume); - _unitOfWork.VolumeRepository.Update(volume); - _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); - } - return false; - } + null, volume.Created, forceUpdate)) return Task.FromResult(false); - if (!volume.CoverImageLocked) - { - // For cover selection, chapters need to try for issue 1 first, then fallback to first sort order - volume.Chapters ??= new List(); - var firstChapter = volume.Chapters.FirstOrDefault(x => x.MinNumber.Is(1f)); - if (firstChapter == null) - { - firstChapter = volume.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default); - if (firstChapter == null) return false; - } - - volume.CoverImage = firstChapter.CoverImage; - } - _imageService.UpdateColorScape(volume); + volume.Chapters ??= new List(); + var firstChapter = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default); + if (firstChapter == null) return Task.FromResult(false); + volume.CoverImage = firstChapter.CoverImage; _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); - return true; + return Task.FromResult(true); } /// @@ -179,34 +121,19 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private void UpdateSeriesCoverImage(Series? series, bool forceUpdate, bool forceColorScape = false) + private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate) { - if (series == null) return; + if (series == null) return Task.CompletedTask; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), + if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), null, series.Created, forceUpdate, series.CoverImageLocked)) - { - // Check if we don't have a primary/seconary color - if (NeedsColorSpace(series, forceColorScape)) - { - _imageService.UpdateColorScape(series); - _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); - } + return Task.CompletedTask; - return; - } - - series.Volumes ??= []; + series.Volumes ??= new List(); series.CoverImage = series.GetCoverImage(); - if (series.CoverImage == null) - { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); - } - - _imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); + return Task.CompletedTask; } @@ -216,7 +143,7 @@ public class MetadataService : IMetadataService /// /// /// - private void ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) + private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize) { _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); try @@ -229,8 +156,8 @@ public class MetadataService : IMetadataService var index = 0; foreach (var chapter in volume.Chapters) { - var chapterUpdated = UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize, forceColorScape); - // If cover was update, either the file has changed or first scan, and we should force a metadata update + var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize); + // If cover was update, either the file has changed or first scan and we should force a metadata update UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated); if (index == 0 && chapterUpdated) { @@ -240,7 +167,7 @@ public class MetadataService : IMetadataService index++; } - var volumeUpdated = UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate, forceColorScape); + var volumeUpdated = await UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate); if (volumeIndex == 0 && volumeUpdated) { firstVolumeUpdated = true; @@ -248,7 +175,7 @@ public class MetadataService : IMetadataService volumeIndex++; } - UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate, forceColorScape); + await UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate); } catch (Exception ex) { @@ -263,10 +190,9 @@ public class MetadataService : IMetadataService /// This can be heavy on memory first run /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - /// Force updating colorscape [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false) + public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false) { var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (library == null) return; @@ -314,7 +240,7 @@ public class MetadataService : IMetadataService try { - ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); + await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize); } catch (Exception ex) { @@ -344,7 +270,7 @@ public class MetadataService : IMetadataService await _unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated(); await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); } @@ -355,8 +281,7 @@ public class MetadataService : IMetadataService /// /// /// Overrides any cache logic and forces execution - /// Will ensure that the colorscape is regenerated - public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true) + public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true) { var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) @@ -365,12 +290,10 @@ public class MetadataService : IMetadataService return; } - // TODO: Cache this because it's called a lot during scans var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var encodeFormat = settings.EncodeMediaAs; var coverImageSize = settings.CoverImageSize; - - await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape); + await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate); } /// @@ -379,14 +302,13 @@ public class MetadataService : IMetadataService /// A full Series, with metadata, chapters, etc /// When saving the file, what encoding should be used /// - /// Forces just colorscape generation - public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true) + public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); - ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); + await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize); if (_unitOfWork.HasChanges()) diff --git a/API/Services/PersonService.cs b/API/Services/PersonService.cs deleted file mode 100644 index ff0049cbe..000000000 --- a/API/Services/PersonService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; - -namespace API.Services; - -public interface IPersonService -{ - /// - /// Adds src as an alias to dst, this is a destructive operation - /// - /// Merged person - /// Remaining person - /// The entities passed as arguments **must** include all relations - /// - Task MergePeopleAsync(Person src, Person dst); - - /// - /// Adds the alias to the person, requires that the aliases are not shared with anyone else - /// - /// This method does NOT commit changes - /// - /// - /// - Task UpdatePersonAliasesAsync(Person person, IList aliases); -} - -public class PersonService(IUnitOfWork unitOfWork): IPersonService -{ - - public async Task MergePeopleAsync(Person src, Person dst) - { - if (dst.Id == src.Id) return; - - if (string.IsNullOrWhiteSpace(dst.Description) && !string.IsNullOrWhiteSpace(src.Description)) - { - dst.Description = src.Description; - } - - if (dst.MalId == 0 && src.MalId != 0) - { - dst.MalId = src.MalId; - } - - if (dst.AniListId == 0 && src.AniListId != 0) - { - dst.AniListId = src.AniListId; - } - - if (dst.HardcoverId == null && src.HardcoverId != null) - { - dst.HardcoverId = src.HardcoverId; - } - - if (dst.Asin == null && src.Asin != null) - { - dst.Asin = src.Asin; - } - - if (dst.CoverImage == null && src.CoverImage != null) - { - dst.CoverImage = src.CoverImage; - } - - MergeChapterPeople(dst, src); - MergeSeriesMetadataPeople(dst, src); - - dst.Aliases.Add(new PersonAliasBuilder(src.Name).Build()); - - foreach (var alias in src.Aliases) - { - dst.Aliases.Add(alias); - } - - unitOfWork.PersonRepository.Remove(src); - unitOfWork.PersonRepository.Update(dst); - await unitOfWork.CommitAsync(); - } - - private static void MergeChapterPeople(Person dst, Person src) - { - - foreach (var chapter in src.ChapterPeople) - { - var alreadyPresent = dst.ChapterPeople - .Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role); - - if (alreadyPresent) continue; - - dst.ChapterPeople.Add(new ChapterPeople - { - Role = chapter.Role, - ChapterId = chapter.ChapterId, - Person = dst, - KavitaPlusConnection = chapter.KavitaPlusConnection, - OrderWeight = chapter.OrderWeight, - }); - } - } - - private static void MergeSeriesMetadataPeople(Person dst, Person src) - { - foreach (var series in src.SeriesMetadataPeople) - { - var alreadyPresent = dst.SeriesMetadataPeople - .Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role); - - if (alreadyPresent) continue; - - dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople - { - SeriesMetadataId = series.SeriesMetadataId, - Role = series.Role, - Person = dst, - KavitaPlusConnection = series.KavitaPlusConnection, - OrderWeight = series.OrderWeight, - }); - } - } - - public async Task UpdatePersonAliasesAsync(Person person, IList aliases) - { - var normalizedAliases = aliases - .Select(a => a.ToNormalized()) - .Where(a => !string.IsNullOrEmpty(a) && a != person.NormalizedName) - .ToList(); - - if (normalizedAliases.Count == 0) - { - person.Aliases = []; - return true; - } - - var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases); - others = others.Where(p => p.Id != person.Id).ToList(); - - if (others.Count != 0) return false; - - person.Aliases = aliases.Select(a => new PersonAliasBuilder(a).Build()).ToList(); - - return true; - } -} diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 0777e1baa..8b8d046c1 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1,281 +1,52 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.Entities; using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; using API.Helpers.Builders; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using AutoMapper; using Flurl.Http; -using Hangfire; using Kavita.Common; +using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Plus; -#nullable enable +/// +/// Used for matching and fetching metadata on a series +/// +internal class ExternalMetadataIdsDto +{ + public long? MalId { get; set; } + public int? AniListId { get; set; } + public string? SeriesName { get; set; } + public string? LocalizedSeriesName { get; set; } + public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown; +} public interface IExternalMetadataService { - Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); - Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType); - Task FetchExternalDataTask(); - /// - /// This is an entry point and provides a level of protection against calling upstream API. Will only allow 100 new - /// series to fetch data within a day and enqueues background jobs at certain times to fetch that data. - /// - /// - /// - /// If the fetch was made - Task FetchSeriesMetadata(int seriesId, LibraryType libraryType); - - Task> GetStacksForUser(int userId); - Task> MatchSeries(MatchSeriesDto dto); - Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId); - Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); - Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); + Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); } public class ExternalMetadataService : IExternalMetadataService { private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly ILicenseService _licenseService; - private readonly IScrobblingService _scrobblingService; - private readonly IEventHub _eventHub; - private readonly ICoverDbService _coverDbService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; - private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); - public static readonly HashSet NonEligibleLibraryTypes = - [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; - private readonly SeriesDetailPlusDto _defaultReturn = new() - { - Series = null, - Recommendations = null, - Ratings = [], - Reviews = [] - }; - // Allow 50 requests per 24 hours - private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false); - private static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$"); - public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, - ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService, - IKavitaPlusApiService kavitaPlusApiService) + public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; _logger = logger; - _mapper = mapper; - _licenseService = licenseService; - _scrobblingService = scrobblingService; - _eventHub = eventHub; - _coverDbService = coverDbService; - _kavitaPlusApiService = kavitaPlusApiService; - FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } - /// - /// Checks if the library type is allowed to interact with Kavita+ - /// - /// - /// - public static bool IsPlusEligible(LibraryType type) - { - return !NonEligibleLibraryTypes.Contains(type); - } - - /// - /// This is a task that runs on a schedule and slowly fetches data from Kavita+ to keep - /// data in the DB non-stale and fetched. - /// - /// To avoid blasting Kavita+ API, this only processes 25 records. The goal is to slowly build out/refresh the data - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task FetchExternalDataTask() - { - // Find all Series that are eligible and limit - var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25); - if (ids.Count == 0) return; - ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true); - - _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+: {Ids}", ids.Count, string.Join(',', ids)); - var count = 0; - var successfulMatches = new List(); - var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids); - foreach (var seriesId in ids) - { - var libraryType = libTypes[seriesId]; - var success = await FetchSeriesMetadata(seriesId, libraryType); - if (success) - { - count++; - successfulMatches.Add(seriesId); - } - await Task.Delay(6000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request - } - _logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} / {Total} series data from Kavita+: {Ids}", count, ids.Count, string.Join(',', successfulMatches)); - } - - - /// - /// Fetches data from Kavita+ - /// - /// - /// - /// If a successful match was made - public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) - { - if (!IsPlusEligible(libraryType)) return false; - if (!await _licenseService.HasActiveLicense()) return false; - - // Generate key based on seriesId and libraryType or any unique identifier for the request - // Check if the request is allowed based on the rate limit - if (!RateLimiter.TryAcquire(string.Empty)) - { - // Request not allowed due to rate limit - _logger.LogInformation("Rate Limit hit for Kavita+ prefetch"); - return false; - } - - // Prefetch SeriesDetail data - return await GetSeriesDetailPlus(seriesId, libraryType) != null; - } - - public async Task> GetStacksForUser(int userId) - { - if (!await _licenseService.HasActiveLicense()) return ArraySegment.Empty; - - // See if this user has Mal account on record - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null || string.IsNullOrEmpty(user.MalUserName) || string.IsNullOrEmpty(user.MalAccessToken)) - { - _logger.LogInformation("User is attempting to fetch MAL Stacks, but missing information on their account"); - return ArraySegment.Empty; - } - try - { - _logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName); - - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var result = await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); - - if (result == null) - { - return ArraySegment.Empty; - } - - return result; - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Fetching Kavita+ for MAL Stacks for user {UserName} failed", user.MalUserName); - return ArraySegment.Empty; - } - } - - /// - /// Returns the match results for a Series from UI Flow - /// - /// - /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable - /// - /// - /// - public async Task> MatchSeries(MatchSeriesDto dto) - { - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); - if (series == null) return []; - - var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); - var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); - - var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); - var otherNames = ExtractAlternativeNames(series); - - var year = series.Metadata.ReleaseYear; - if (year == 0 && format == PlusMediaFormat.Comic && !string.IsNullOrWhiteSpace(series.Name)) - { - var potentialYear = Parser.ParseYear(series.Name); - if (!string.IsNullOrEmpty(potentialYear)) - { - year = int.Parse(potentialYear); - } - } - - var matchRequest = new MatchSeriesRequestDto() - { - Format = format, - Query = dto.Query, - SeriesName = series.Name, - AlternativeNames = otherNames, - Year = year, - AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), - MalId = potentialMalId ?? ScrobblingService.GetMalId(series) - }; - - try - { - var results = await _kavitaPlusApiService.MatchSeries(matchRequest); - - // Some summaries can contain multiple
s, we need to ensure it's only 1 - foreach (var result in results) - { - result.Series.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(result.Series.Summary)); - } - - return results; - } - catch (Exception ex) - { - _logger.LogError(ex, "An error happened during the request to Kavita+ API"); - } - - return ArraySegment.Empty; - } - - private static List ExtractAlternativeNames(Series series) - { - List altNames = [series.LocalizedName, series.OriginalName]; - return altNames.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); - } - - - /// - /// Retrieves Metadata about a Recommended External Series - /// - /// - /// - /// - /// - /// public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId) { if (!aniListId.HasValue && !malId.HasValue) @@ -283,1587 +54,12 @@ public class ExternalMetadataService : IExternalMetadataService throw new KavitaException("Unable to find valid information from url for External Load"); } - // This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB - var details = await GetSeriesDetail(aniListId, malId, seriesId); - - return details; + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + return await GetSeriesDetail(license, aniListId, malId, seriesId); } - /// - /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings - /// - /// - /// - /// - public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType) - { - if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense()) return _defaultReturn; - - // Check blacklist (bad matches) or if there is a don't match - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - if (series == null || !series.WillScrobble()) return _defaultReturn; - - var needsRefresh = - await _unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(seriesId); - - if (!needsRefresh) - { - // Convert into DTOs and return - return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId); - } - - var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId); - if (data == null) return _defaultReturn; - - // Get from Kavita+ API the Full Series metadata with rec/rev and cache to ExternalMetadata tables - try - { - return await FetchExternalMetadataForSeries(seriesId, libraryType, data); - } - catch (KavitaException ex) - { - _logger.LogError(ex, "Rate limit hit fetching metadata"); - // This can happen when we hit rate limit - return _defaultReturn; - } - } - - /// - /// This will override any sort of matching that was done prior and force it to be what the user Selected - /// - /// - /// - /// - /// - public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId) - { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); - if (series == null) return; - - // Remove from Blacklist - series.IsBlacklisted = false; - series.DontMatch = false; - _unitOfWork.SeriesRepository.Update(series); - - // Refetch metadata with a Direct lookup - try - { - var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, - new PlusSeriesRequestDto() - { - AniListId = aniListId, - MalId = malId, - CbrId = cbrId, - MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed - }); - - if (metadata.Series == null) - { - _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series with Id: {AniListId}/{MalId}/{CbrId}", - series.Name, aniListId, malId, cbrId); - return; - } - - // Find all scrobble events and rewrite them to be the correct - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); - _unitOfWork.ScrobbleRepository.Remove(events); - - // Find all scrobble errors and remove them - var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId); - _unitOfWork.ScrobbleRepository.Remove(errors); - - await _unitOfWork.CommitAsync(); - - // Regenerate all events for the series for all users - BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistoryForSeries(seriesId)); - - // Name can be null on Series even with a direct match - _logger.LogInformation("Matched {SeriesName} with Kavita+ Series {MatchSeriesName}", series.Name, - metadata.Series.Name); - } - catch (KavitaException ex) - { - // We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times - _logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name); - // Fire SignalR event about this - await _eventHub.SendMessageAsync(MessageFactory.ExternalMatchRateLimitError, - MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name)); - } - } - - /// - /// Sets a series to Don't Match and removes all previously cached - /// - /// - public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch) - { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.ExternalMetadata); - if (series == null) return; - - _logger.LogInformation("User has asked Kavita to stop matching/scrobbling on {SeriesName}", series.Name); - - series.DontMatch = dontMatch; - - if (dontMatch) - { - // When we set as DontMatch, we will clear existing External Metadata - var externalSeriesMetadata = await GetOrCreateExternalSeriesMetadataForSeries(seriesId, series); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(series.ExternalSeriesMetadata); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations); - } - - _unitOfWork.SeriesRepository.Update(series); - - await _unitOfWork.CommitAsync(); - } - - /// - /// Requests the full SeriesDetail (rec, review, metadata) data for a Series. Will save to ExternalMetadata tables. - /// - /// - /// - /// - /// - private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data) - { - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); - if (series == null) - { - return _defaultReturn; - } - - try - { - _logger.LogDebug("Fetching Kavita+ Series Detail data for {SeriesName}", string.IsNullOrEmpty(data.SeriesName) ? data.AniListId : data.SeriesName); - SeriesDetailPlusApiDto? result = null; - - try - { - // This returns an AniListSeries and Match returns ExternalSeriesDto - result = await _kavitaPlusApiService.GetSeriesDetail(data); - } - catch (FlurlHttpException ex) - { - var errorMessage = await ex.GetResponseStringAsync(); - // Trim quotes if the response is a JSON string - errorMessage = errorMessage.Trim('"'); - - if (ex.StatusCode == 400) - { - if (errorMessage.Contains("Too many Requests")) - { - _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); - await Task.Delay(3000); - - result = await _kavitaPlusApiService.GetSeriesDetail(data); - } - else if (errorMessage.Contains("Unknown Series")) - { - series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); - } - } - } - - if (result == null) - { - _logger.LogInformation("Hit rate limit twice, try again later"); - return _defaultReturn; - } - - - // Clear out existing results - var externalSeriesMetadata = await GetOrCreateExternalSeriesMetadataForSeries(seriesId, series); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations); - - externalSeriesMetadata.ExternalReviews = result.Reviews.Select(r => - { - var review = _mapper.Map(r); - review.SeriesId = externalSeriesMetadata.SeriesId; - return review; - }).ToList(); - - externalSeriesMetadata.ExternalRatings = result.Ratings.Select(r => - { - var rating = _mapper.Map(r); - rating.SeriesId = externalSeriesMetadata.SeriesId; - rating.ProviderUrl = r.ProviderUrl; - return rating; - }).ToList(); - - - // Recommendations - externalSeriesMetadata.ExternalRecommendations ??= []; - var recs = await ProcessRecommendations(libraryType, result.Recommendations, externalSeriesMetadata); - - var extRatings = externalSeriesMetadata.ExternalRatings - .Where(r => r.AverageScore > 0) - .ToList(); - - externalSeriesMetadata.ValidUntilUtc = DateTime.UtcNow.Add(_externalSeriesMetadataCache); - externalSeriesMetadata.AverageExternalRating = extRatings.Count != 0 ? (int) extRatings - .Average(r => r.AverageScore) : 0; - - if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value; - if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value; - if (result.CbrId.HasValue) externalSeriesMetadata.CbrId = result.CbrId.Value; - - // If there is metadata and the user has metadata download turned on - var madeMetadataModification = false; - if (result.Series != null && series.Library.AllowMetadataMatching) - { - externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - - try - { - madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId); - if (madeMetadataModification) - { - _unitOfWork.SeriesRepository.Update(series); - _unitOfWork.SeriesRepository.Update(series.Metadata); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when trying to write Series metadata from Kavita+"); - } - - } - - // WriteExternalMetadataToSeries will commit but not always - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - } - - if (madeMetadataModification) - { - // Inform the UI of the update - await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false); - } - - return new SeriesDetailPlusDto() - { - Recommendations = recs, - Ratings = result.Ratings, - Reviews = externalSeriesMetadata.ExternalReviews.Select(r => _mapper.Map(r)), - Series = result.Series - }; - } - catch (FlurlHttpException ex) - { - var errorMessage = await ex.GetResponseStringAsync(); - // Trim quotes if the response is a JSON string - errorMessage = errorMessage.Trim('"'); - - if (ex.StatusCode == 500) - { - return _defaultReturn; - } - - if (ex.StatusCode == 400 && errorMessage.Contains("Too many Requests")) - { - throw new KavitaException("Too many requests, slow down"); - } - } - catch (Exception ex) - { - if (ex.Message.Contains("Too Many Requests")) - { - throw new KavitaException("Too many requests, slow down"); - } - - _logger.LogError(ex, "Unable to fetch external series metadata from Kavita+"); - } - - // Blacklist the series as it wasn't found in Kavita+ - series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); - - return _defaultReturn; - } - - /// - /// Given external metadata from Kavita+, write as much as possible to the Kavita series as possible - /// - /// - /// - /// - public async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId) - { - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - if (!settings.Enabled) return false; - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Related); - if (series == null) return false; - - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - - _logger.LogInformation("Writing External metadata to Series {SeriesName}", series.Name); - - var madeModification = false; - var processedGenres = new List(); - var processedTags = new List(); - - madeModification = UpdateSummary(series, settings, externalMetadata) || madeModification; - madeModification = UpdateReleaseYear(series, settings, externalMetadata) || madeModification; - madeModification = UpdateLocalizedName(series, settings, externalMetadata) || madeModification; - madeModification = await UpdatePublicationStatus(series, settings, externalMetadata) || madeModification; - - // Apply field mappings - GenerateGenreAndTagLists(externalMetadata, settings, ref processedTags, ref processedGenres); - - madeModification = await UpdateGenres(series, settings, externalMetadata, processedGenres) || madeModification; - madeModification = await UpdateTags(series, settings, externalMetadata, processedTags) || madeModification; - madeModification = UpdateAgeRating(series, settings, processedGenres.Concat(processedTags)) || madeModification; - - var staff = await SetNameAndAddAliases(settings, externalMetadata.Staff); - - madeModification = await UpdateWriters(series, settings, staff) || madeModification; - madeModification = await UpdateArtists(series, settings, staff) || madeModification; - madeModification = await UpdateCharacters(series, settings, externalMetadata.Characters) || madeModification; - - madeModification = await UpdateRelationships(series, settings, externalMetadata.Relations, defaultAdmin) || madeModification; - madeModification = await UpdateCoverImage(series, settings, externalMetadata) || madeModification; - - madeModification = await UpdateChapters(series, settings, externalMetadata) || madeModification; - - return madeModification; - } - - private async Task> SetNameAndAddAliases(MetadataSettingsDto settings, IList? staff) - { - if (staff == null || staff.Count == 0) return []; - - var nameMappings = staff.Select(s => new - { - Staff = s, - PreferredName = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}", - AlternativeName = !settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}" - }).ToList(); - - var preferredNames = nameMappings.Select(n => n.PreferredName.ToNormalized()).Distinct().ToList(); - var alternativeNames = nameMappings.Select(n => n.AlternativeName.ToNormalized()).Distinct().ToList(); - - var existingPeople = await _unitOfWork.PersonRepository.GetPeopleByNames(preferredNames.Union(alternativeNames).ToList()); - var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); - - var modified = false; - foreach (var mapping in nameMappings) - { - mapping.Staff.Name = mapping.PreferredName; - - if (existingPeopleDictionary.ContainsKey(mapping.PreferredName.ToNormalized())) - { - continue; - } - - - if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person)) - { - modified = true; - person.Aliases.Add(new PersonAliasBuilder(mapping.PreferredName).Build()); - } - } - - if (modified) - { - await _unitOfWork.CommitAsync(); - } - - return [.. staff]; - } - - private static void GenerateGenreAndTagLists(ExternalSeriesDetailDto externalMetadata, MetadataSettingsDto settings, - ref List processedTags, ref List processedGenres) - { - externalMetadata.Tags ??= []; - externalMetadata.Genres ??= []; - - var mappings = ApplyFieldMappings(externalMetadata.Tags.Select(t => t.Name), MetadataFieldType.Tag, settings.FieldMappings); - if (mappings.TryGetValue(MetadataFieldType.Tag, out var tagsToTags)) - { - processedTags.AddRange(tagsToTags); - } - if (mappings.TryGetValue(MetadataFieldType.Genre, out var tagsToGenres)) - { - processedGenres.AddRange(tagsToGenres); - } - - mappings = ApplyFieldMappings(externalMetadata.Genres, MetadataFieldType.Genre, settings.FieldMappings); - if (mappings.TryGetValue(MetadataFieldType.Tag, out var genresToTags)) - { - processedTags.AddRange(genresToTags); - } - if (mappings.TryGetValue(MetadataFieldType.Genre, out var genresToGenres)) - { - processedGenres.AddRange(genresToGenres); - } - - processedTags = ApplyBlackWhiteList(settings, MetadataFieldType.Tag, processedTags); - processedGenres = ApplyBlackWhiteList(settings, MetadataFieldType.Genre, processedGenres); - } - - private async Task UpdateRelationships(Series series, MetadataSettingsDto settings, IList? externalMetadataRelations, AppUser defaultAdmin) - { - if (!settings.EnableRelationships) return false; - - if (externalMetadataRelations == null || externalMetadataRelations.Count == 0 || defaultAdmin == null) - { - return false; - } - - foreach (var relation in externalMetadataRelations.Where(r => r.Relation != RelationKind.Parent)) - { - List names = new [] {relation.SeriesName.PreferredTitle, relation.SeriesName.RomajiTitle, relation.SeriesName.EnglishTitle, relation.SeriesName.NativeTitle}.Where(s => !string.IsNullOrEmpty(s)).ToList()!; - var relatedSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName( - names, - relation.PlusMediaFormat.GetMangaFormats(), - defaultAdmin.Id, - relation.AniListId, - SeriesIncludes.Related); - - // Skip if no related series found or series is the parent - if (relatedSeries == null || relatedSeries.Id == series.Id || relation.Relation == RelationKind.Parent) continue; - - // Check if the relationship already exists - var relationshipExists = series.Relations.Any(r => - r.TargetSeriesId == relatedSeries.Id && r.RelationKind == relation.Relation); - - if (relationshipExists) continue; - - // Add new relationship - var newRelation = new SeriesRelation - { - RelationKind = relation.Relation, - TargetSeriesId = relatedSeries.Id, - SeriesId = series.Id, - }; - series.Relations.Add(newRelation); - - // Handle sequel/prequel: add reverse relationship - if (relation.Relation is RelationKind.Prequel or RelationKind.Sequel) - { - var reverseExists = relatedSeries.Relations.Any(r => - r.TargetSeriesId == series.Id && r.RelationKind == GetReverseRelation(relation.Relation)); - - if (!reverseExists) - { - var reverseRelation = new SeriesRelation - { - RelationKind = GetReverseRelation(relation.Relation), - TargetSeriesId = series.Id, - SeriesId = relatedSeries.Id, - }; - relatedSeries.Relations.Add(reverseRelation); - _unitOfWork.SeriesRepository.Attach(reverseRelation); - } - } - - _unitOfWork.SeriesRepository.Update(series); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - } - - return true; - } - - private async Task UpdateCharacters(Series series, MetadataSettingsDto settings, IList? externalCharacters) - { - if (!settings.EnablePeople) return false; - - if (externalCharacters == null || externalCharacters.Count == 0) return false; - - if (series.Metadata.CharacterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) - { - return false; - } - - if (!settings.IsPersonAllowed(PersonRole.Character)) - { - return false; - } - - series.Metadata.People ??= []; - - var characters = externalCharacters - .Select(w => new PersonDto() - { - Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), - Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), - }) - .Concat(series.Metadata.People - .Where(p => p.Role == PersonRole.Character) - // Need to ensure existing people are retained, but we overwrite anything from a bad match - .Where(p => !p.KavitaPlusConnection) - .Select(p => _mapper.Map(p.Person)) - ) - .DistinctBy(p => Parser.Normalize(p.Name)) - .ToList(); - - if (characters.Count == 0) return false; - - await SeriesService.HandlePeopleUpdateAsync(series.Metadata, characters, PersonRole.Character, _unitOfWork); - - foreach (var spPerson in series.Metadata.People.Where(p => p.Role == PersonRole.Character)) - { - // Set a sort order based on their role - var characterMeta = externalCharacters.FirstOrDefault(c => c.Name == spPerson.Person.Name); - spPerson.OrderWeight = 0; - - if (characterMeta != null) - { - spPerson.KavitaPlusConnection = true; - - spPerson.OrderWeight = characterMeta.Role switch - { - CharacterRole.Main => 0, - CharacterRole.Supporting => 1, - CharacterRole.Background => 2, - _ => 99 // Default for unknown roles - }; - } - } - - // Download the image and save it - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - - foreach (var character in externalCharacters) - { - var aniListId = ScrobblingService.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); - if (aniListId <= 0) continue; - var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); - if (person != null && !string.IsNullOrEmpty(character.ImageUrl) && string.IsNullOrEmpty(person.CoverImage)) - { - await _coverDbService.SetPersonCoverByUrl(person, character.ImageUrl, false); - } - } - - series.Metadata.AddKPlusOverride(MetadataSettingField.People); - return true; - } - - private async Task UpdateArtists(Series series, MetadataSettingsDto settings, List staff) - { - if (!settings.EnablePeople) return false; - - - var upstreamArtists = staff - .Where(s => s.Role is "Art" or "Story & Art") - .ToList(); - - if (upstreamArtists.Count == 0) return false; - - if (series.Metadata.CoverArtistLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) - { - return false; - } - - if (!settings.IsPersonAllowed(PersonRole.CoverArtist)) - { - return false; - } - - series.Metadata.People ??= []; - var artists = upstreamArtists - .Select(w => new PersonDto() - { - Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), - Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), - }) - .Concat(series.Metadata.People - .Where(p => p.Role == PersonRole.CoverArtist) - .Where(p => !p.KavitaPlusConnection) - .Select(p => _mapper.Map(p.Person)) - ) - .DistinctBy(p => Parser.Normalize(p.Name)) - .ToList(); - - await SeriesService.HandlePeopleUpdateAsync(series.Metadata, artists, PersonRole.CoverArtist, _unitOfWork); - - foreach (var person in series.Metadata.People.Where(p => p.Role == PersonRole.CoverArtist)) - { - var meta = upstreamArtists.FirstOrDefault(c => c.Name == person.Person.Name); - person.OrderWeight = 0; - if (meta != null) - { - person.KavitaPlusConnection = true; - } - } - - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - - await DownloadAndSetPersonCovers(upstreamArtists); - - series.Metadata.AddKPlusOverride(MetadataSettingField.People); - return true; - } - - private async Task UpdateWriters(Series series, MetadataSettingsDto settings, List staff) - { - if (!settings.EnablePeople) return false; - - var upstreamWriters = staff - .Where(s => s.Role is "Story" or "Story & Art") - .ToList(); - - if (upstreamWriters.Count == 0) return false; - - if (series.Metadata.WriterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) - { - return false; - } - - if (!settings.IsPersonAllowed(PersonRole.Writer)) - { - return false; - } - - series.Metadata.People ??= []; - var writers = upstreamWriters - .Select(w => new PersonDto() - { - Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), - Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), - }) - .Concat(series.Metadata.People - .Where(p => p.Role == PersonRole.Writer) - .Where(p => !p.KavitaPlusConnection) - .Select(p => _mapper.Map(p.Person)) - ) - .DistinctBy(p => Parser.Normalize(p.Name)) - .ToList(); - - - await SeriesService.HandlePeopleUpdateAsync(series.Metadata, writers, PersonRole.Writer, _unitOfWork); - - foreach (var person in series.Metadata.People.Where(p => p.Role == PersonRole.Writer)) - { - var meta = upstreamWriters.FirstOrDefault(c => c.Name == person.Person.Name); - person.OrderWeight = 0; - if (meta != null) - { - person.KavitaPlusConnection = true; - } - } - - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - - await DownloadAndSetPersonCovers(upstreamWriters); - series.Metadata.AddKPlusOverride(MetadataSettingField.People); - return true; - } - - private async Task UpdateTags(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata, List processedTags) - { - externalMetadata.Tags ??= []; - - if (!settings.EnableTags || processedTags.Count == 0) return false; - - if (series.Metadata.TagsLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Tags)) - { - return false; - } - - _logger.LogDebug("Found {TagCount} tags for {SeriesName}", processedTags.Count, series.Name); - var madeModification = false; - var allTags = (await _unitOfWork.TagRepository.GetAllTagsByNameAsync(processedTags.Select(Parser.Normalize))) - .ToList(); - series.Metadata.Tags ??= []; - - TagHelper.UpdateTagList(processedTags, series, allTags, tag => - { - series.Metadata.Tags.Add(tag); - madeModification = true; - }, () => series.Metadata.TagsLocked = true); - - if (madeModification) - { - series.Metadata.AddKPlusOverride(MetadataSettingField.Tags); - } - - return madeModification; - } - - private static List ApplyBlackWhiteList(MetadataSettingsDto settings, MetadataFieldType fieldType, List processedStrings) - { - return fieldType switch - { - MetadataFieldType.Genre => processedStrings.Distinct() - .Where(g => settings.Blacklist.Count == 0 || !settings.Blacklist.Contains(g)) - .ToList(), - MetadataFieldType.Tag => processedStrings.Distinct() - .Where(g => settings.Blacklist.Count == 0 || !settings.Blacklist.Contains(g)) - .Where(g => settings.Whitelist.Count == 0 || settings.Whitelist.Contains(g)) - .ToList(), - _ => throw new ArgumentOutOfRangeException(nameof(fieldType), fieldType, null) - }; - } - - private async Task UpdateGenres(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata, List processedGenres) - { - externalMetadata.Genres ??= []; - - if (!settings.EnableGenres || processedGenres.Count == 0) return false; - - if (series.Metadata.GenresLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Genres)) - { - return false; - } - - _logger.LogDebug("Found {GenreCount} genres for {SeriesName}", processedGenres.Count, series.Name); - var madeModification = false; - var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(processedGenres.Select(Parser.Normalize))).ToList(); - series.Metadata.Genres ??= []; - var exisitingGenres = series.Metadata.Genres; - - GenreHelper.UpdateGenreList(processedGenres, series, allGenres, genre => - { - series.Metadata.Genres.Add(genre); - madeModification = true; - }, () => series.Metadata.GenresLocked = true); - - foreach (var genre in exisitingGenres) - { - if (series.Metadata.Genres.FirstOrDefault(g => g.NormalizedTitle == genre.NormalizedTitle) != null) continue; - series.Metadata.Genres.Add(genre); - madeModification = true; - } - - if (madeModification) - { - series.Metadata.AddKPlusOverride(MetadataSettingField.Genres); - } - - return madeModification; - } - - private async Task UpdatePublicationStatus(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) - { - if (!settings.EnablePublicationStatus) return false; - - if (series.Metadata.PublicationStatusLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.PublicationStatus)) - { - return false; - } - - try - { - var chapters = - (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Chapters))!.Volumes - .SelectMany(v => v.Chapters).ToList(); - var status = DeterminePublicationStatus(series, chapters, externalMetadata); - - series.Metadata.PublicationStatus = status; - series.Metadata.PublicationStatusLocked = true; - series.Metadata.AddKPlusOverride(MetadataSettingField.PublicationStatus); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue determining Publication Status for Series {SeriesName} ({SeriesId})", series.Name, series.Id); - } - - return false; - } - - private bool UpdateAgeRating(Series series, MetadataSettingsDto settings, IEnumerable allExternalTags) - { - - if (series.Metadata.AgeRatingLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.AgeRating)) - { - return false; - } - - try - { - // Determine Age Rating - var totalTags = allExternalTags - .Concat(series.Metadata.Genres.Select(g => g.Title)) - .Concat(series.Metadata.Tags.Select(g => g.Title)); - - var ageRating = DetermineAgeRating(totalTags, settings.AgeRatingMappings); - if (series.Metadata.AgeRating <= ageRating) - { - series.Metadata.AgeRating = ageRating; - series.Metadata.AddKPlusOverride(MetadataSettingField.AgeRating); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue determining Age Rating for Series {SeriesName} ({SeriesId})", series.Name, series.Id); - } - - return false; - } - - - private async Task UpdateChapters(Series series, MetadataSettingsDto settings, - ExternalSeriesDetailDto externalMetadata) - { - if (externalMetadata.PlusMediaFormat != PlusMediaFormat.Comic) return false; - - if (externalMetadata.ChapterDtos == null || externalMetadata.ChapterDtos.Count == 0) return false; - - // Get all volumes and chapters - var madeModification = false; - var allChapters = await _unitOfWork.ChapterRepository.GetAllChaptersForSeries(series.Id); - - var matchedChapters = allChapters - .Join( - externalMetadata.ChapterDtos, - chapter => chapter.Range, - dto => dto.IssueNumber, - (chapter, dto) => (chapter, dto) // Create a tuple of matched pairs - ) - .ToList(); - - foreach (var (chapter, potentialMatch) in matchedChapters) - { - _logger.LogDebug("Updating {ChapterNumber} with metadata", chapter.Range); - - // Write the metadata - madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification; - madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification; - madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification; - - var hasUpdatedPublisher = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher); - if (hasUpdatedPublisher) chapter.AddKPlusOverride(MetadataSettingField.ChapterPublisher); - madeModification = hasUpdatedPublisher || madeModification; - - madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification; - madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; - - madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; - madeModification = await UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification; - - _unitOfWork.ChapterRepository.Update(chapter); - await _unitOfWork.CommitAsync(); - } - - return madeModification; - } - - private async Task UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) - { - if (!settings.Enabled) return false; - - if (metadata.UserReviews.Count == 0 && metadata.CriticReviews.Count == 0) - { - return false; - } - - var madeModification = false; - - #region Review - - // Remove existing Reviews - var existingReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReview(chapter.Id); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingReviews); - - - List externalReviews = []; - externalReviews.AddRange(metadata.CriticReviews - .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) - .Select(r => - { - var review = _mapper.Map(r); - review.ChapterId = chapter.Id; - review.Authority = RatingAuthority.Critic; - CleanCbrReview(ref review); - return review; - })); - externalReviews.AddRange(metadata.UserReviews - .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) - .Select(r => - { - var review = _mapper.Map(r); - review.ChapterId = chapter.Id; - review.Authority = RatingAuthority.User; - CleanCbrReview(ref review); - return review; - })); - - chapter.ExternalReviews = externalReviews; - madeModification = externalReviews.Count > 0; - _logger.LogDebug("Added {Count} reviews for chapter {ChapterId}", externalReviews.Count, chapter.Id); - #endregion - - #region Rating - - // C# can't make the implicit conversation here - float? averageCriticRating = metadata.CriticReviews.Count > 0 ? metadata.CriticReviews.Average(r => r.Rating) : null; - float? averageUserRating = metadata.UserReviews.Count > 0 ? metadata.UserReviews.Average(r => r.Rating) : null; - - var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings); - - chapter.ExternalRatings = []; - - if (averageUserRating != null) - { - chapter.ExternalRatings.Add(new ExternalRating - { - AverageScore = (int) averageUserRating, - Provider = ScrobbleProvider.Cbr, - Authority = RatingAuthority.User, - ProviderUrl = metadata.IssueUrl, - - }); - chapter.AverageExternalRating = averageUserRating.Value; - } - - if (averageCriticRating != null) - { - chapter.ExternalRatings.Add(new ExternalRating - { - AverageScore = (int) averageCriticRating, - Provider = ScrobbleProvider.Cbr, - Authority = RatingAuthority.Critic, - ProviderUrl = metadata.IssueUrl, - - }); - } - - madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification; - - #endregion - - return madeModification; - } - - private static void CleanCbrReview(ref ExternalReview review) - { - // CBR has Read Full Review which links to site, but we already have that - review.Body = review.Body.Replace("Read Full Review", string.Empty).TrimEnd(); - review.RawBody = review.RawBody.Replace("Read Full Review", string.Empty).TrimEnd(); - review.BodyJustText = review.BodyJustText.Replace("Read Full Review", string.Empty).TrimEnd(); - } - - - private static bool UpdateChapterSummary(Chapter chapter, MetadataSettingsDto settings, string? summary) - { - if (!settings.EnableChapterSummary) return false; - - if (string.IsNullOrEmpty(summary)) return false; - - if (chapter.SummaryLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary)) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(summary) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary)) - { - return false; - } - - chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary)); - chapter.AddKPlusOverride(MetadataSettingField.ChapterSummary); - return true; - } - - private static bool UpdateChapterTitle(Chapter chapter, MetadataSettingsDto settings, string? title, string seriesName) - { - if (!settings.EnableChapterTitle) return false; - - if (string.IsNullOrEmpty(title)) return false; - - if (chapter.TitleNameLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle)) - { - return false; - } - - if (!title.Contains(seriesName) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle)) - { - return false; - } - - chapter.TitleName = title; - chapter.AddKPlusOverride(MetadataSettingField.ChapterTitle); - return true; - } - - private static bool UpdateChapterReleaseDate(Chapter chapter, MetadataSettingsDto settings, DateTime? releaseDate) - { - if (!settings.EnableChapterReleaseDate) return false; - - if (releaseDate == null || releaseDate == DateTime.MinValue) return false; - - if (chapter.ReleaseDateLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate)) - { - return false; - } - - if (!HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate)) - { - return false; - } - - chapter.ReleaseDate = releaseDate.Value; - chapter.AddKPlusOverride(MetadataSettingField.ChapterReleaseDate); - return true; - } - - private async Task UpdateChapterPublisher(Chapter chapter, MetadataSettingsDto settings, string? publisher) - { - if (!settings.EnableChapterPublisher) return false; - - if (string.IsNullOrEmpty(publisher)) return false; - - if (chapter.PublisherLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher)) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(publisher) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher)) - { - return false; - } - - // Some publishers (CBR) can be represented as Boom! Studios/Boom! Town imprint, so let's handle that appropriately - if (publisher.Contains('/') || publisher.Contains("imprint", StringComparison.InvariantCultureIgnoreCase)) - { - var imprint = publisher.Split('/')[1].Replace("imprint", string.Empty); - return await UpdateChapterPeople(chapter, settings, PersonRole.Publisher, [publisher]) || - await UpdateChapterPeople(chapter, settings, PersonRole.Imprint, [imprint]); - } - - return await UpdateChapterPeople(chapter, settings, PersonRole.Publisher, [publisher]); - } - - private async Task UpdateChapterCoverImage(Chapter chapter, MetadataSettingsDto settings, string? coverUrl) - { - if (!settings.EnableChapterCoverImage) return false; - - if (string.IsNullOrEmpty(coverUrl)) return false; - - if (chapter.CoverImageLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterCovers)) - { - return false; - } - - if (string.IsNullOrEmpty(coverUrl)) - { - return false; - } - - await DownloadChapterCovers(chapter, coverUrl); - chapter.AddKPlusOverride(MetadataSettingField.ChapterCovers); - return true; - } - - private async Task UpdateChapterPeople(Chapter chapter, MetadataSettingsDto settings, PersonRole role, IList? staff) - { - if (!settings.EnablePeople) return false; - - if (staff?.Count == 0) return false; - - if (chapter.IsPersonRoleLocked(role) && !HasForceOverride(settings, chapter, MetadataSettingField.People)) - { - return false; - } - - if (!settings.IsPersonAllowed(role) && role != PersonRole.Publisher) - { - return false; - } - - chapter.People ??= []; - var people = staff! - .Select(w => new PersonDto() - { - Name = w.Trim(), - }) - .Concat(chapter.People - .Where(p => p.Role == role) - .Where(p => !p.KavitaPlusConnection) - .Select(p => _mapper.Map(p.Person)) - ) - .DistinctBy(p => Parser.Normalize(p.Name)) - .ToList(); - - await PersonHelper.UpdateChapterPeopleAsync(chapter, staff ?? [], role, _unitOfWork); - - foreach (var person in chapter.People.Where(p => p.Role == role)) - { - var meta = people.FirstOrDefault(c => c.Name == person.Person.Name); - person.OrderWeight = 0; - - if (meta != null) - { - person.KavitaPlusConnection = true; - } - } - - _unitOfWork.ChapterRepository.Update(chapter); - await _unitOfWork.CommitAsync(); - - return true; - } - - private async Task UpdateCoverImage(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) - { - if (!settings.EnableCoverImage) return false; - - if (string.IsNullOrEmpty(externalMetadata.CoverUrl)) return false; - - if (series.CoverImageLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Covers)) - { - return false; - } - - if (string.IsNullOrEmpty(externalMetadata.CoverUrl)) - { - return false; - } - - await DownloadSeriesCovers(series, externalMetadata.CoverUrl); - series.Metadata.AddKPlusOverride(MetadataSettingField.Covers); - return true; - } - - - private static bool UpdateReleaseYear(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) - { - if (!settings.EnableStartDate) return false; - - if (!externalMetadata.StartDate.HasValue) return false; - - if (series.Metadata.ReleaseYearLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate)) - { - return false; - } - - if (series.Metadata.ReleaseYear != 0 && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate)) - { - return false; - } - - series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year; - series.Metadata.AddKPlusOverride(MetadataSettingField.StartDate); - return true; - } - - private static bool UpdateLocalizedName(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) - { - if (!settings.EnableLocalizedName) return false; - - if (series.LocalizedNameLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName)) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(series.LocalizedName) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName)) - { - return false; - } - - // We need to make the best appropriate guess - if (externalMetadata.Name == series.Name) - { - // Choose closest (usually last) synonym - var validSynonyms = externalMetadata.Synonyms - .Where(IsRomanCharacters) - .Where(s => s.ToNormalized() != series.Name.ToNormalized()) - .ToList(); - - if (validSynonyms.Count == 0) return false; - - series.LocalizedName = validSynonyms[^1]; - series.LocalizedNameLocked = true; - } - else if (IsRomanCharacters(externalMetadata.Name)) - { - series.LocalizedName = externalMetadata.Name; - series.LocalizedNameLocked = true; - } - - - series.Metadata.AddKPlusOverride(MetadataSettingField.LocalizedName); - return true; - } - - private static bool UpdateSummary(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) - { - if (!settings.EnableSummary) return false; - - if (string.IsNullOrEmpty(externalMetadata.Summary)) return false; - - if (series.Metadata.SummaryLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary)) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(series.Metadata.Summary) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary)) - { - return false; - } - - series.Metadata.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(externalMetadata.Summary)); - series.Metadata.AddKPlusOverride(MetadataSettingField.Summary); - return true; - } - - - private static RelationKind GetReverseRelation(RelationKind relation) - { - return relation switch - { - RelationKind.Prequel => RelationKind.Sequel, - RelationKind.Sequel => RelationKind.Prequel, - _ => relation // For other relationships, no reverse needed - }; - } - - private async Task DownloadSeriesCovers(Series series, string coverUrl) - { - try - { - // Only choose the better image if we're overriding a user provided cover - await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, !series.Metadata.HasSetKPlusMetadata(MetadataSettingField.Covers)); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception downloading cover image for Series {SeriesName} ({SeriesId})", series.Name, series.Id); - } - } - - private async Task DownloadChapterCovers(Chapter chapter, string coverUrl) - { - try - { - await _coverDbService.SetChapterCoverByUrl(chapter, coverUrl, false, true); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception downloading cover image for Chapter {ChapterName} ({SeriesId})", chapter.Range, chapter.Id); - } - } - - private async Task DownloadAndSetPersonCovers(List people) - { - foreach (var staff in people) - { - var aniListId = ScrobblingService.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); - if (aniListId is null or <= 0) continue; - var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value); - if (person == null || string.IsNullOrEmpty(staff.ImageUrl) || - !string.IsNullOrEmpty(person.CoverImage) || staff.ImageUrl.EndsWith("default.jpg")) continue; - - try - { - await _coverDbService.SetPersonCoverByUrl(person, staff.ImageUrl, false, true); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving cover image for Person {PersonName} ({PersonId})", person.Name, person.Id); - } - - } - } - - private PublicationStatus DeterminePublicationStatus(Series series, List chapters, ExternalSeriesDetailDto externalMetadata) - { - try - { - // Determine the expected total count based on local metadata - series.Metadata.TotalCount = Math.Max( - chapters.Max(chapter => chapter.TotalCount), - externalMetadata.Volumes > 0 ? externalMetadata.Volumes : externalMetadata.Chapters - ); - - // The actual number of count's defined across all chapter's metadata - series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); - - var nonSpecialVolumes = series.Volumes - .Where(v => v.MaxNumber.IsNot(Parser.SpecialVolumeNumber)) - .ToList(); - - var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); - var maxChapter = (int)chapters.Max(c => c.MaxNumber); - - if (series.Format is MangaFormat.Epub or MangaFormat.Pdf && chapters.Count == 1) - { - series.Metadata.MaxCount = 1; - } - else if (series.Metadata.TotalCount <= 1 && chapters is [{ IsSpecial: true }]) - { - series.Metadata.MaxCount = series.Metadata.TotalCount; - } - else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && - maxVolume <= series.Metadata.TotalCount && maxVolume != Parser.DefaultChapterNumber) - { - series.Metadata.MaxCount = maxVolume; - } - else if (maxVolume == series.Metadata.TotalCount) - { - series.Metadata.MaxCount = maxVolume; - } - else - { - series.Metadata.MaxCount = maxChapter; - } - - var status = PublicationStatus.OnGoing; - - var hasExternalCounts = externalMetadata.Volumes > 0 || externalMetadata.Chapters > 0; - - if (hasExternalCounts) - { - status = PublicationStatus.Ended; - - if (IsSeriesCompleted(series, chapters, externalMetadata, maxVolume)) - { - status = PublicationStatus.Completed; - } - } - - return status; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "There was an issue determining Publication Status"); - } - - return PublicationStatus.OnGoing; - } - - /// - /// Returns true if the series should be marked as completed, checks loosey with chapter and series numbers. - /// Respects Specials to reach the required amount. - /// - /// - /// - /// - /// - /// - /// Updates MaxCount and TotalCount if a loosey check is used to set as completed - public static bool IsSeriesCompleted(Series series, List chapters, ExternalSeriesDetailDto externalMetadata, int maxVolumes) - { - // A series is completed if exactly the amount is found - if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) - { - return true; - } - - // If volumes are collected, check if we reach the required volumes by including specials, and decimal volumes - // - // TODO BUG: If the series has specials, that are not included in the external count. But you do own them - // This may mark the series as completed pre-maturely - // Note: I've currently opted to keep this an equals to prevent the above bug from happening - // We *could* change this to >= in the future in case this is reported by users - // If we do; test IsSeriesCompleted_Volumes_TooManySpecials needs to be updated - if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == series.Volumes.Count) - { - series.Metadata.MaxCount = series.Volumes.Count; - series.Metadata.TotalCount = series.Volumes.Count; - return true; - } - - // Note: If Kavita has specials, we should be lenient and ignore for the volume check - var volumeModifier = series.Volumes.Any(v => v.Name == Parser.SpecialVolume) ? 1 : 0; - var modifiedMinVolumeCount = series.Volumes.Count - volumeModifier; - if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == modifiedMinVolumeCount) - { - series.Metadata.MaxCount = modifiedMinVolumeCount; - series.Metadata.TotalCount = modifiedMinVolumeCount; - return true; - } - - // If no volumes are collected, the series is completed if we reach or exceed the external chapters - if (maxVolumes == Parser.DefaultChapterNumber && series.Metadata.MaxCount >= externalMetadata.Chapters) - { - series.Metadata.TotalCount = series.Metadata.MaxCount; - return true; - } - - // If no volumes are collected, the series is complete if we reach or exceed the external chapters while including - // prologues, and extra chapters - if (maxVolumes == Parser.DefaultChapterNumber && chapters.Count >= externalMetadata.Chapters) - { - series.Metadata.TotalCount = chapters.Count; - series.Metadata.MaxCount = chapters.Count; - return true; - } - - - return false; - } - - private static Dictionary> ApplyFieldMappings(IEnumerable values, MetadataFieldType sourceType, List mappings) - { - var result = new Dictionary>(); - - foreach (var field in Enum.GetValues()) - { - result[field] = []; - } - - foreach (var value in values) - { - var mapping = mappings.FirstOrDefault(m => - m.SourceType == sourceType && - m.SourceValue.Equals(value, StringComparison.OrdinalIgnoreCase)); - - if (mapping != null && !string.IsNullOrWhiteSpace(mapping.DestinationValue)) - { - var targetType = mapping.DestinationType; - - if (!mapping.ExcludeFromSource) - { - result[sourceType].Add(mapping.SourceValue); - } - - result[targetType].Add(mapping.DestinationValue); - } - else - { - // If no mapping, keep the original value - result[sourceType].Add(value); - } - } - - // Ensure distinct - foreach (var key in result.Keys) - { - result[key] = result[key].Distinct().ToList(); - } - - return result; - } - - - /// - /// Returns the highest age rating from all tags/genres based on user-supplied mappings - /// - /// A combo of all tags/genres - /// - /// - public static AgeRating DetermineAgeRating(IEnumerable values, Dictionary mappings) - { - // Find highest age rating from mappings - mappings ??= new Dictionary(); - - return values - .Select(v => mappings.TryGetValue(v, out var mapping) ? mapping : AgeRating.Unknown) - .DefaultIfEmpty(AgeRating.Unknown) - .Max(); - } - - - /// - /// Gets from DB or creates a new one with just SeriesId - /// - /// - /// - /// - private async Task GetOrCreateExternalSeriesMetadataForSeries(int seriesId, Series series) - { - var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId); - if (externalSeriesMetadata != null) return externalSeriesMetadata; - - externalSeriesMetadata = new ExternalSeriesMetadata() - { - SeriesId = seriesId, - }; - series.ExternalSeriesMetadata = externalSeriesMetadata; - _unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata); - - return externalSeriesMetadata; - } - - private async Task ProcessRecommendations(LibraryType libraryType, IEnumerable recs, - ExternalSeriesMetadata externalSeriesMetadata) - { - var recDto = new RecommendationDto() - { - ExternalSeries = new List(), - OwnedSeries = new List() - }; - - // NOTE: This can result in a series being recommended that shares the same name but different format - foreach (var rec in recs) - { - // Find the series based on name and type and that the user has access too - var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames, - libraryType, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), - ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); - - if (seriesForRec != null) - { - recDto.OwnedSeries.Add(seriesForRec); - externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation() - { - SeriesId = seriesForRec.Id, - AniListId = rec.AniListId, - MalId = rec.MalId, - Name = seriesForRec.Name, - Url = rec.SiteUrl, - CoverUrl = rec.CoverUrl, - Summary = rec.Summary, - Provider = rec.Provider - }); - continue; - } - - // We can show this based on user permissions - if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue; - recDto.ExternalSeries.Add(new ExternalSeriesDto() - { - Name = string.IsNullOrEmpty(rec.Name) ? rec.RecommendationNames.First() : rec.Name, - Url = rec.SiteUrl, - CoverUrl = rec.CoverUrl, - Summary = rec.Summary, - AniListId = rec.AniListId, - MalId = rec.MalId - }); - externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation() - { - SeriesId = null, - AniListId = rec.AniListId, - MalId = rec.MalId, - Name = rec.Name, - Url = rec.SiteUrl, - CoverUrl = rec.CoverUrl, - Summary = rec.Summary, - Provider = rec.Provider - }); - } - - recDto.OwnedSeries = recDto.OwnedSeries.DistinctBy(s => s.Id).OrderBy(r => r.Name).ToList(); - recDto.ExternalSeries = recDto.ExternalSeries.DistinctBy(s => s.Name.ToNormalized()).OrderBy(r => r.Name).ToList(); - - return recDto; - } - - - /// - /// This is to get series information for the recommendation drawer on Kavita - /// - /// This uses a different API that series detail - /// - /// - /// - /// - /// - private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId) { var payload = new ExternalMetadataIdsDto() { @@ -1872,11 +68,9 @@ public class ExternalMetadataService : IExternalMetadataService SeriesName = string.Empty, LocalizedSeriesName = string.Empty }; - if (seriesId is > 0) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, - SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series != null) { if (payload.AniListId <= 0) @@ -1889,17 +83,22 @@ public class ExternalMetadataService : IExternalMetadataService } payload.SeriesName = series.Name; payload.LocalizedSeriesName = series.LocalizedName; - payload.PlusMediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format); + payload.PlusMediaFormat = ConvertToMediaFormat(series.Library.Type, series.Format); } } try { - var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); - - ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); - - return ret; + return await (Configuration.KavitaPlusApiUrl + "/api/metadata/series/detail") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(payload) + .ReceiveJson(); } catch (Exception e) @@ -1910,9 +109,14 @@ public class ExternalMetadataService : IExternalMetadataService return null; } - private static bool HasForceOverride(MetadataSettingsDto settings, IHasKPlusMetadata kPlusMetadata, - MetadataSettingField field) + private static MediaFormat ConvertToMediaFormat(LibraryType libraryType, MangaFormat seriesFormat) { - return settings.HasOverride(field) || kPlusMetadata.HasSetKPlusMetadata(field); + return libraryType switch + { + LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga, + LibraryType.Comic => MediaFormat.Comic, + LibraryType.Book => MediaFormat.Book, + _ => MediaFormat.Unknown + }; } } diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/API/Services/Plus/KavitaPlusApiService.cs deleted file mode 100644 index ec4f414c3..000000000 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ /dev/null @@ -1,126 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Extensions; -using Flurl.Http; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services.Plus; - -/// -/// All Http requests to K+ should be contained in this service, the service will not handle any errors. -/// This is expected from the caller. -/// -public interface IKavitaPlusApiService -{ - Task HasTokenExpired(string license, string token, ScrobbleProvider provider); - Task GetRateLimit(string license, string token); - Task PostScrobbleUpdate(ScrobbleDto data, string license); - Task> GetMalStacks(string malUsername, string license); - Task> MatchSeries(MatchSeriesRequestDto request); - Task GetSeriesDetail(PlusSeriesRequestDto request); - Task GetSeriesDetailById(ExternalMetadataIdsDto request); -} - -public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService -{ - private const string ScrobblingPath = "/api/scrobbling/"; - - public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) - { - var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); - var str = await res.GetStringAsync(); - return bool.Parse(str); - } - - public async Task GetRateLimit(string license, string token) - { - var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); - var str = await res.GetStringAsync(); - return int.Parse(str); - } - - public async Task PostScrobbleUpdate(ScrobbleDto data, string license) - { - return await PostAndReceive(ScrobblingPath + "update", data, license); - } - - public async Task> GetMalStacks(string malUsername, string license) - { - return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" - .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); - } - - public async Task> MatchSeries(MatchSeriesRequestDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson>(); - } - - public async Task GetSeriesDetail(PlusSeriesRequestDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson(); - } - - public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson(); - } - - /// - /// Send a GET request to K+ - /// - /// only path of the uri, the host is added - /// - /// - /// - private static async Task Get(string url, string license, string? aniListToken = null) - { - return await (Configuration.KavitaPlusApiUrl + url) - .WithKavitaPlusHeaders(license, aniListToken) - .GetAsync(); - } - - /// - /// Send a POST request to K+ - /// - /// only path of the uri, the host is added - /// - /// - /// - /// Return type - /// - private static async Task PostAndReceive(string url, object body, string license, string? aniListToken = null) - { - return await (Configuration.KavitaPlusApiUrl + url) - .WithKavitaPlusHeaders(license, aniListToken) - .PostJsonAsync(body) - .ReceiveJson(); - } -} diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index 91f5a8fdd..8bb845da3 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -1,21 +1,18 @@ using System; -using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; using API.DTOs.Account; -using API.DTOs.KavitaPlus.License; +using API.DTOs.License; using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks; using EasyCaching.Core; using Flurl.Http; +using Hangfire; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Microsoft.Extensions.Logging; namespace API.Services.Plus; -#nullable enable internal class RegisterLicenseResponseDto { @@ -26,29 +23,28 @@ internal class RegisterLicenseResponseDto public interface ILicenseService { - //Task ValidateLicenseStatus(); + Task ValidateLicenseStatus(); Task RemoveLicense(); - Task AddLicense(string license, string email, string? discordId); + Task AddLicense(string license, string email); Task HasActiveLicense(bool forceCheck = false); - Task HasActiveSubscription(string? license); - Task ResetLicense(string license, string email); - Task GetLicenseInfo(bool forceCheck = false); } -public class LicenseService( - IEasyCachingProviderFactory cachingProviderFactory, - IUnitOfWork unitOfWork, - ILogger logger, - IVersionUpdaterService versionUpdaterService) - : ILicenseService +public class LicenseService : ILicenseService { + private readonly IEasyCachingProviderFactory _cachingProviderFactory; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; private readonly TimeSpan _licenseCacheTimeout = TimeSpan.FromHours(8); - public const string Cron = "0 */9 * * *"; - /// - /// Cache key for if license is valid or not - /// - public const string CacheKey = "license"; - private const string LicenseInfoCacheKey = "license-info"; + public const string Cron = "0 */4 * * *"; + private const string CacheKey = "license"; + + + public LicenseService(IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork, ILogger logger) + { + _cachingProviderFactory = cachingProviderFactory; + _unitOfWork = unitOfWork; + _logger = logger; + } /// @@ -62,7 +58,13 @@ public class LicenseService( try { var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check") - .WithKavitaPlusHeaders(license) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) .PostJsonAsync(new LicenseValidDto() { License = license, @@ -73,7 +75,7 @@ public class LicenseService( } catch (Exception e) { - logger.LogError(e, "An error happened during the request to Kavita+ API"); + _logger.LogError(e, "An error happened during the request to Kavita+ API"); throw; } } @@ -84,19 +86,24 @@ public class LicenseService( /// /// /// - private async Task RegisterLicense(string license, string email, string? discordId) + private async Task RegisterLicense(string license, string email) { if (string.IsNullOrWhiteSpace(license) || string.IsNullOrWhiteSpace(email)) return string.Empty; try { var response = await (Configuration.KavitaPlusApiUrl + "/api/license/register") - .WithKavitaPlusHeaders(license) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) .PostJsonAsync(new EncryptLicenseDto() { License = license.Trim(), InstallId = HashUtil.ServerToken(), - EmailId = email.Trim(), - DiscordId = discordId?.Trim() + EmailId = email.Trim() }) .ReceiveJson(); @@ -105,204 +112,90 @@ public class LicenseService( return response.EncryptedLicense; } - logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response.ErrorMessage); + _logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response.ErrorMessage); throw new KavitaException(response.ErrorMessage); } catch (FlurlHttpException e) { - logger.LogError(e, "An error happened during the request to Kavita+ API"); + _logger.LogError(e, "An error happened during the request to Kavita+ API"); return string.Empty; } } - /// /// Checks licenses and updates cache /// - /// Skip what's in cache - /// + /// Expected to be called at startup and on reoccurring basis + public async Task ValidateLicenseStatus() + { + var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + try + { + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + if (string.IsNullOrEmpty(license.Value)) { + await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); + return; + } + + _logger.LogInformation("Validating Kavita+ License"); + + await provider.FlushAsync(); + var isValid = await IsLicenseValid(license.Value); + await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout); + + _logger.LogInformation("Validating Kavita+ License - Complete"); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins"); + await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); + BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30)); + } + } + + public async Task RemoveLicense() + { + var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + serverSetting.Value = string.Empty; + _unitOfWork.SettingsRepository.Update(serverSetting); + await _unitOfWork.CommitAsync(); + var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + await provider.RemoveAsync(CacheKey); + } + + public async Task AddLicense(string license, string email) + { + var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var lic = await RegisterLicense(license, email); + if (string.IsNullOrWhiteSpace(lic)) + throw new KavitaException("unable-to-register-k+"); + serverSetting.Value = lic; + _unitOfWork.SettingsRepository.Update(serverSetting); + await _unitOfWork.CommitAsync(); + } + public async Task HasActiveLicense(bool forceCheck = false) { - var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); if (!forceCheck) { var cacheValue = await provider.GetAsync(CacheKey); if (cacheValue.HasValue) return cacheValue.Value; } - var result = false; try { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - result = await IsLicenseValid(serverSetting.Value); + var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var result = await IsLicenseValid(serverSetting.Value); + await provider.FlushAsync(); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + return result; } catch (Exception ex) { - logger.LogError(ex, "There was an issue connecting to Kavita+"); - } - finally - { - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); - } - - return result; - } - - /// - /// Checks if the sub is active and caches the result. This should not be used too much over cache as it will skip backend caching. - /// - /// - /// - public async Task HasActiveSubscription(string? license) - { - if (string.IsNullOrWhiteSpace(license)) return false; - try - { - var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check-sub") - .WithKavitaPlusHeaders(license) - .PostJsonAsync(new LicenseValidDto() - { - License = license, - InstallId = HashUtil.ServerToken() - }) - .ReceiveString(); - - var result = bool.Parse(response); - - var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); - - return result; - } - catch (Exception e) - { - logger.LogError(e, "An error happened during the request to Kavita+ API"); - return false; - } - } - - public async Task RemoveLicense() - { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - serverSetting.Value = string.Empty; - unitOfWork.SettingsRepository.Update(serverSetting); - await unitOfWork.CommitAsync(); - - var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); - - - } - - public async Task AddLicense(string license, string email, string? discordId) - { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var lic = await RegisterLicense(license, email, discordId); - if (string.IsNullOrWhiteSpace(lic)) - throw new KavitaException("unable-to-register-k+"); - serverSetting.Value = lic; - unitOfWork.SettingsRepository.Update(serverSetting); - await unitOfWork.CommitAsync(); - } - - - - public async Task ResetLicense(string license, string email) - { - try - { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset") - .WithKavitaPlusHeaders(encryptedLicense.Value) - .PostJsonAsync(new ResetLicenseDto() - { - License = license.Trim(), - InstallId = HashUtil.ServerToken(), - EmailId = email - }) - .ReceiveString(); - - if (string.IsNullOrEmpty(response)) - { - var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); - return true; - } - - logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response); - throw new KavitaException(response); - } - catch (FlurlHttpException e) - { - logger.LogError(e, "An error happened during the request to Kavita+ API"); + _logger.LogError(ex, "There was an issue connecting to Kavita+"); } return false; } - - /// - /// Fetches information about the license from Kavita+. If there is no license or an exception, will return null and can be assumed it is not active - /// - /// - /// - public async Task GetLicenseInfo(bool forceCheck = false) - { - // Check if there is a license - var hasLicense = - !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)) - .Value); - - if (!hasLicense) return null; - - // Check the cache - var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LicenseInfo); - if (!forceCheck) - { - var cacheValue = await licenseInfoProvider.GetAsync(LicenseInfoCacheKey); - if (cacheValue.HasValue) return cacheValue.Value; - } - - // TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking - - try - { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var response = await (Configuration.KavitaPlusApiUrl + "/api/license/info") - .WithKavitaPlusHeaders(encryptedLicense.Value) - .GetJsonAsync(); - - // This indicates a mismatch on installId or no active subscription - if (response == null) return null; - - // Ensure that current version is within the 3 version limit. Don't count Nightly releases or Hotfixes - var releases = await versionUpdaterService.GetAllReleases(); - response.IsValidVersion = releases - .Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases - .Where(r => !r.IsPrerelease) // Ensure we don't take current nightlies within the current/last stable - .Take(3) - .All(r => new Version(r.UpdateVersion) <= BuildInfo.Version); - - response.HasLicense = hasLicense; - - // Cache if the license is valid here as well - var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout); - - // Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2 - if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2)) - { - await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout); - } - - return response; - } - catch (FlurlHttpException e) - { - logger.LogError(e, "An error happened during the request to Kavita+ API"); - } - - return null; - } } diff --git a/API/Services/Plus/RatingService.cs b/API/Services/Plus/RatingService.cs new file mode 100644 index 000000000..0993948fd --- /dev/null +++ b/API/Services/Plus/RatingService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs; +using API.Entities; +using API.Entities.Enums; +using API.Helpers; +using API.Helpers.Builders; +using Flurl.Http; +using Kavita.Common; +using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Helpers; +using Microsoft.Extensions.Logging; + +namespace API.Services.Plus; + +public interface IRatingService +{ + Task> GetRatings(int seriesId); +} + +public class RatingService : IRatingService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public RatingService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + } + + public async Task> GetRatings(int seriesId) + { + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes); + + // Don't send any ratings back for Comic libraries as Kavita+ doesn't have any providers for that + if (series == null || series.Library.Type == LibraryType.Comic) return ImmutableList.Empty; + return await GetRatings(license.Value, series); + } + + private async Task> GetRatings(string license, Series series) + { + try + { + return await (Configuration.KavitaPlusApiUrl + "/api/rating") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) + .ReceiveJson>(); + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to Kavita+ API"); + } + + return new List(); + } +} diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs new file mode 100644 index 000000000..87fbfabaa --- /dev/null +++ b/API/Services/Plus/RecommendationService.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs; +using API.DTOs.Recommendation; +using API.DTOs.Scrobbling; +using API.Entities; +using API.Entities.Enums; +using API.Extensions; +using API.Helpers.Builders; +using Flurl.Http; +using Kavita.Common; +using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Helpers; +using Microsoft.Extensions.Logging; + +namespace API.Services.Plus; + +public record PlusSeriesDto +{ + public int? AniListId { get; set; } + public long? MalId { get; set; } + public string? GoogleBooksId { get; set; } + public string SeriesName { get; set; } + public string? AltSeriesName { get; set; } + public MediaFormat MediaFormat { get; set; } + /// + /// Optional but can help with matching + /// + public int? ChapterCount { get; set; } + /// + /// Optional but can help with matching + /// + public int? VolumeCount { get; set; } + public int? Year { get; set; } +} + +internal record MediaRecommendationDto +{ + public int Rating { get; set; } + public IEnumerable RecommendationNames { get; set; } = null!; + public string Name { get; set; } + public string CoverUrl { get; set; } + public string SiteUrl { get; set; } + public string? Summary { get; set; } + public int? AniListId { get; set; } + public long? MalId { get; set; } +} + +public interface IRecommendationService +{ + Task GetRecommendationsForSeries(int userId, int seriesId); +} + +public class RecommendationService : IRecommendationService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public RecommendationService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + } + + public async Task GetRecommendationsForSeries(int userId, int seriesId) + { + var series = + await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Volumes | SeriesIncludes.Chapters); + if (series == null || series.Library.Type == LibraryType.Comic) return new RecommendationDto(); + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var canSeeExternalSeries = user is {AgeRestriction: AgeRating.NotApplicable} && + await _unitOfWork.UserRepository.IsUserAdminAsync(user); + + var recDto = new RecommendationDto() + { + ExternalSeries = new List(), + OwnedSeries = new List() + }; + + var recs = await GetRecommendations(license.Value, series); + foreach (var rec in recs) + { + // Find the series based on name and type and that the user has access too + var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIdsForUser(userId, rec.RecommendationNames, + series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), + ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); + + if (seriesForRec != null) + { + recDto.OwnedSeries.Add(seriesForRec); + continue; + } + + if (!canSeeExternalSeries) continue; + // We can show this based on user permissions + if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue; + recDto.ExternalSeries.Add(new ExternalSeriesDto() + { + Name = string.IsNullOrEmpty(rec.Name) ? rec.RecommendationNames.First() : rec.Name, + Url = rec.SiteUrl, + CoverUrl = rec.CoverUrl, + Summary = rec.Summary, + AniListId = rec.AniListId, + MalId = rec.MalId + }); + } + + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, recDto.OwnedSeries); + + recDto.OwnedSeries = recDto.OwnedSeries.DistinctBy(s => s.Id).OrderBy(r => r.Name).ToList(); + recDto.ExternalSeries = recDto.ExternalSeries.DistinctBy(s => s.Name.ToNormalized()).OrderBy(r => r.Name).ToList(); + + return recDto; + } + + + private async Task> GetRecommendations(string license, Series series) + { + try + { + return await (Configuration.KavitaPlusApiUrl + "/api/recommendation") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) + .ReceiveJson>(); + + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to Kavita+ API"); + } + + return new List(); + } +} diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index f9c3fdb09..f9b206076 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -1,31 +1,27 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; +using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Entities.Scrobble; -using API.Extensions; using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Flurl.Http; using Hangfire; using Kavita.Common; +using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Services.Plus; -#nullable enable /// /// Misleading name but is the source of data (like a review coming from AniList) @@ -38,187 +34,75 @@ public enum ScrobbleProvider Kavita = 0, AniList = 1, Mal = 2, - [Obsolete("No longer supported")] - GoogleBooks = 3, - Cbr = 4 } public interface IScrobblingService { - /// - /// An automated job that will run against all user's tokens and validate if they are still active - /// - /// This service can validate without license check as the task which calls will be guarded - /// Task CheckExternalAccessTokens(); - - /// - /// Checks if the token has expired with , if it has double checks with K+, - /// otherwise return false. - /// - /// - /// - /// - /// Returns true if there is no license present Task HasTokenExpired(int userId, ScrobbleProvider provider); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - /// Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); - /// - /// NOP, until hardcover support has been worked out - /// - /// - /// - /// - /// - /// - Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// + Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody); Task ScrobbleReadingUpdate(int userId, int seriesId); - /// - /// Creates an or for - /// the given series - /// - /// - /// - /// - /// - /// Only the result of both WantToRead types is send to K+ Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); - /// - /// Removed all processed events that are at least 7 days old - /// - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public Task ClearProcessedEvents(); - - /// - /// Makes K+ requests for all non-processed events until rate limits are reached - /// - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ProcessUpdatesSinceLastSync(); - Task CreateEventsFromExistingHistory(int userId = 0); - Task CreateEventsFromExistingHistoryForSeries(int seriesId); - Task ClearEventsForSeries(int userId, int seriesId); -} - -/// -/// Context used when syncing scrobble events. Do NOT reuse between syncs -/// -public class ScrobbleSyncContext -{ - public required List ReadEvents {get; init;} - public required List RatingEvents {get; init;} - /// Do not use this as events to send to K+, use - public required List AddToWantToRead {get; init;} - /// Do not use this as events to send to K+, use - public required List RemoveWantToRead {get; init;} - /// - /// Final events list if all AddTo- and RemoveWantToRead would be processed sequentially - /// - public required List Decisions {get; init;} - /// - /// K+ license - /// - public required string License { get; init; } - /// - /// Maps userId to left over request amount - /// - public required Dictionary RateLimits { get; init; } - - /// - /// All users being scrobbled for - /// - public List Users { get; set; } = []; - /// - /// Amount of already processed events - /// - public int ProgressCounter { get; set; } - - /// - /// Sum of all events to process - /// - public int TotalCount => ReadEvents.Count + RatingEvents.Count + AddToWantToRead.Count + RemoveWantToRead.Count; } public class ScrobblingService : IScrobblingService { private readonly IUnitOfWork _unitOfWork; + private readonly ITokenService _tokenService; private readonly IEventHub _eventHub; private readonly ILogger _logger; private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; - private readonly IEmailService _emailService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; - public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; - public const string AniListStaffWebsite = "https://anilist.co/staff/"; - public const string AniListCharacterWebsite = "https://anilist.co/character/"; - - private static readonly Dictionary WeblinkExtractionMap = new() + private static readonly IDictionary WeblinkExtractionMap = new Dictionary() { {AniListWeblinkWebsite, 0}, {MalWeblinkWebsite, 0}, {GoogleBooksWeblinkWebsite, 0}, - {MangaDexWeblinkWebsite, 0}, - {AniListStaffWebsite, 0}, - {AniListCharacterWebsite, 0}, + }; - private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) + private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) - private static readonly IList BookProviders = []; - private static readonly IList LightNovelProviders = - [ + private static readonly IList BookProviders = new List() + { ScrobbleProvider.AniList - ]; - private static readonly IList ComicProviders = []; - private static readonly IList MangaProviders = (List) - [ScrobbleProvider.AniList]; + }; + private static readonly IList ComicProviders = new List(); + private static readonly IList MangaProviders = new List() + { + ScrobbleProvider.AniList + }; - private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling"; - private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling"; - private const string InvalidKPlusLicenseErrorMessage = "Kavita+ subscription no longer active"; - private const string ReviewFailedErrorMessage = "Review was unable to be saved due to upstream requirements"; - private const string BadPayLoadErrorMessage = "Bad payload from Scrobble Provider"; - - - public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger logger, - ILicenseService licenseService, ILocalizationService localizationService, IEmailService emailService, - IKavitaPlusApiService kavitaPlusApiService) + public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService, + IEventHub eventHub, ILogger logger, ILicenseService licenseService, + ILocalizationService localizationService) { _unitOfWork = unitOfWork; + _tokenService = tokenService; _eventHub = eventHub; _logger = logger; _licenseService = licenseService; _localizationService = localizationService; - _emailService = emailService; - _kavitaPlusApiService = kavitaPlusApiService; - FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } - #region Access token checks /// /// An automated job that will run against all user's tokens and validate if they are still active @@ -231,71 +115,13 @@ public class ScrobblingService : IScrobblingService var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); foreach (var user in users) { - if (string.IsNullOrEmpty(user.AniListAccessToken)) continue; - - var tokenExpiry = JwtHelper.GetTokenExpiry(user.AniListAccessToken); - - // Send early reminder 5 days before token expiry - if (await ShouldSendEarlyReminder(user.Id, tokenExpiry)) - { - await _emailService.SendTokenExpiringSoonEmail(user.Id, ScrobbleProvider.AniList); - } - - // Send expiration notification after token expiry - if (await ShouldSendExpirationReminder(user.Id, tokenExpiry)) - { - await _emailService.SendTokenExpiredEmail(user.Id, ScrobbleProvider.AniList); - } - - // Check token validity - if (JwtHelper.IsTokenValid(user.AniListAccessToken)) continue; - - _logger.LogInformation( - "User {UserName}'s AniList token has expired or is expiring in a few days! They need to regenerate it for scrobbling to work", - user.UserName); - - // Notify user via event - await _eventHub.SendMessageToAsync( - MessageFactory.ScrobblingKeyExpired, - MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), - user.Id); - + if (string.IsNullOrEmpty(user.AniListAccessToken) || !_tokenService.HasTokenExpired(user.AniListAccessToken)) continue; + _logger.LogInformation("User {UserName}'s AniList token has expired! They need to regenerate it for scrobbling to work", user.UserName); + await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired, + MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), user.Id); } } - /// - /// Checks if an early reminder email should be sent. - /// - private async Task ShouldSendEarlyReminder(int userId, DateTime tokenExpiry) - { - var earlyReminderDate = tokenExpiry.AddDays(-5); - if (earlyReminderDate > DateTime.UtcNow) return false; - - var hasAlreadySentReminder = await _unitOfWork.DataContext.EmailHistory - .AnyAsync(h => h.AppUserId == userId && h.Sent && - h.EmailTemplate == EmailService.TokenExpiringSoonTemplate && - h.SendDate >= earlyReminderDate); - - return !hasAlreadySentReminder; - - } - - /// - /// Checks if an expiration notification email should be sent. - /// - private async Task ShouldSendExpirationReminder(int userId, DateTime tokenExpiry) - { - if (tokenExpiry > DateTime.UtcNow) return false; - - var hasAlreadySentExpirationEmail = await _unitOfWork.DataContext.EmailHistory - .AnyAsync(h => h.AppUserId == userId && h.Sent && - h.EmailTemplate == EmailService.TokenExpirationTemplate && - h.SendDate >= tokenExpiry); - - return !hasAlreadySentExpirationEmail; - - } - public async Task HasTokenExpired(int userId, ScrobbleProvider provider) { var token = await GetTokenForProvider(userId, provider); @@ -313,14 +139,25 @@ public class ScrobblingService : IScrobblingService private async Task HasTokenExpired(string token, ScrobbleProvider provider) { - if (string.IsNullOrEmpty(token) || !TokenService.HasTokenExpired(token)) return false; + if (string.IsNullOrEmpty(token) || + !_tokenService.HasTokenExpired(token)) return false; var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); if (string.IsNullOrEmpty(license.Value)) return true; try { - return await _kavitaPlusApiService.HasTokenExpired(license.Value, token, provider); + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/valid-key?provider=" + provider + "&key=" + token) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license.Value) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .GetStringAsync(); + + return bool.Parse(response); } catch (HttpRequestException e) { @@ -337,40 +174,75 @@ public class ScrobblingService : IScrobblingService private async Task GetTokenForProvider(int userId, ScrobbleProvider provider) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return string.Empty; + if (user == null) return null; return provider switch { ScrobbleProvider.AniList => user.AniListAccessToken, _ => string.Empty - } ?? string.Empty; + }; } - #endregion - - #region Scrobble ingest - - public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) + public async Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody) { - // Currently disabled until at least hardcover is implemented - return Task.CompletedTask; + if (!await _licenseService.HasActiveLicense()) return; + + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); + if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + + _logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name); + if (await CheckIfCanScrobble(userId, seriesId, series)) return; + + if (string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || + reviewTitle.Length > 120 || + reviewTitle.Length < 20)) + { + _logger.LogDebug( + "Rejecting Scrobble event for {Series}. Review is not long enough to meet requirements", series.Name); + return; + } + + var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, + ScrobbleEventType.Review); + if (existingEvt is {IsProcessed: false}) + { + _logger.LogDebug("Overriding Review scrobble event for {Series}", existingEvt.Series.Name); + existingEvt.ReviewBody = reviewBody; + existingEvt.ReviewTitle = reviewTitle; + _unitOfWork.ScrobbleRepository.Update(existingEvt); + await _unitOfWork.CommitAsync(); + return; + } + + var evt = new ScrobbleEvent() + { + SeriesId = series.Id, + LibraryId = series.LibraryId, + ScrobbleEventType = ScrobbleEventType.Review, + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), + AppUserId = userId, + Format = LibraryTypeHelper.GetFormat(series.Library.Type), + ReviewBody = reviewBody, + ReviewTitle = reviewTitle + }; + _unitOfWork.ScrobbleRepository.Attach(evt); + await _unitOfWork.CommitAsync(); + _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId); } public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) { if (!await _licenseService.HasActiveLicense()) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; - - _logger.LogInformation("Processing Scrobbling rating event for {AppUserId} on {SeriesName}", userId, series.Name); - if (await CheckIfCannotScrobble(userId, seriesId, series)) return; + _logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name); + if (await CheckIfCanScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ScoreUpdated, true); + ScrobbleEventType.ScoreUpdated); if (existingEvt is {IsProcessed: false}) { // We need to just update Volume/Chapter number @@ -387,94 +259,65 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ScoreUpdated, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, - Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + Format = LibraryTypeHelper.GetFormat(series.Library.Type), Rating = rating }; _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); + _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {UserId} ", series.Name, userId); } public async Task ScrobbleReadingUpdate(int userId, int seriesId) { if (!await _licenseService.HasActiveLicense()) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; + _logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name); + if (await CheckIfCanScrobble(userId, seriesId, series)) return; - _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); - if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - - var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId); - - var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); - var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); - - // Check if there is an existing not yet processed event, if so update it var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ChapterRead, true); - + ScrobbleEventType.ChapterRead); if (existingEvt is {IsProcessed: false}) { - if (!isAnyProgressOnSeries) - { - _unitOfWork.ScrobbleRepository.Remove(existingEvt); - await _unitOfWork.CommitAsync(); - _logger.LogDebug("Removed scrobble event for {Series} as there is no reading progress", series.Name); - return; - } - // We need to just update Volume/Chapter number var prevChapter = $"{existingEvt.ChapterNumber}"; var prevVol = $"{existingEvt.VolumeNumber}"; - existingEvt.VolumeNumber = volumeNumber; - existingEvt.ChapterNumber = chapterNumber; - + existingEvt.VolumeNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); + existingEvt.ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); _unitOfWork.ScrobbleRepository.Update(existingEvt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Overriding scrobble event for {Series} from vol {PrevVol} ch {PrevChap} -> vol {UpdatedVol} ch {UpdatedChap}", existingEvt.Series.Name, prevVol, prevChapter, existingEvt.VolumeNumber, existingEvt.ChapterNumber); return; } - if (!isAnyProgressOnSeries) - { - // Do not create a new scrobble event if there is no progress - return; - } - try { - var evt = new ScrobbleEvent + var evt = new ScrobbleEvent() { SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ChapterRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, - VolumeNumber = volumeNumber, - ChapterNumber = chapterNumber, - Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + VolumeNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), + ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), + Format = LibraryTypeHelper.GetFormat(series.Library.Type), }; - - if (evt.VolumeNumber is Parser.SpecialVolumeNumber) - { - // We don't process Specials because they will never match on AniList - return; - } - _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {AppUserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); + _logger.LogDebug("Added Scrobbling Read update on {SeriesName} with Userid {UserId} ", series.Name, userId); } catch (Exception ex) { @@ -486,62 +329,466 @@ public class ScrobblingService : IScrobblingService { if (!await _licenseService.HasActiveLicense()) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - if (!series.Library.AllowScrobbling) return; + _logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name); + if (await CheckIfCanScrobble(userId, seriesId, series)) return; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; + var existing = await _unitOfWork.ScrobbleRepository.Exists(userId, series.Id, + onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead); + if (existing) return; - if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - _logger.LogInformation("Processing Scrobbling want-to-read event for {AppUserId} on {SeriesName}", userId, series.Name); - - // Get existing events for this series/user - var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId)) - .Where(e => new[] { ScrobbleEventType.AddWantToRead, ScrobbleEventType.RemoveWantToRead }.Contains(e.ScrobbleEventType)); - - // Remove all existing want-to-read events for this series/user - _unitOfWork.ScrobbleRepository.Remove(existingEvents); - - // Create the new event var evt = new ScrobbleEvent() { SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, - Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + Format = LibraryTypeHelper.GetFormat(series.Library.Type), }; - _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); + _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId); } - #endregion - - #region Scrobble provider methods - - private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) + private async Task CheckIfCanScrobble(int userId, int seriesId, Series series) { - return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || - reviewTitle.Length > 120 || - reviewTitle.Length < 20); + if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) + { + _logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, + userId); + return true; + } + + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); + if (library is not {AllowScrobbling: true}) return true; + if (library.Type == LibraryType.Comic) return true; + return false; } - public static long? GetMalId(Series series) + private async Task GetRateLimit(string license, string aniListToken) { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; + if (string.IsNullOrWhiteSpace(aniListToken)) return 0; + try + { + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/rate-limit?accessToken=" + aniListToken) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .GetStringAsync(); + + return int.Parse(response); + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to Kavita+ API"); + } + + return 0; } - public static int? GetAniListId(Series seriesWithExternalMetadata) + private async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); - return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; + try + { + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/update") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(data) + .ReceiveJson(); + + if (!response.Successful) + { + // Might want to log this under ScrobbleError + if (response.ErrorMessage != null && response.ErrorMessage.Contains("Too Many Requests")) + { + _logger.LogInformation("Hit Too many requests, sleeping to regain requests"); + await Task.Delay(TimeSpan.FromMinutes(1)); + } else if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unknown Series")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to match the series"); + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = "Unknown Series", + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + } else if (response.ErrorMessage != null && response.ErrorMessage.StartsWith("Review")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to save the review"); + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = response.ErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + } + + _logger.LogError("Scrobbling failed due to {ErrorMessage}: {SeriesName}", response.ErrorMessage, data.SeriesName); + throw new KavitaException($"Scrobbling failed due to {response.ErrorMessage}: {data.SeriesName}"); + } + + return response.RateLeft; + } + catch (FlurlHttpException ex) + { + _logger.LogError("Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); + if (ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) + { + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = "Unknown Series", + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + throw new KavitaException("Bad payload from Scrobble Provider"); + } + throw; + } + } + + /// + /// This will back fill events from existing progress history, ratings, and want to read for users that have a valid license + /// + /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user + public async Task CreateEventsFromExistingHistory(int userId = 0) + { + var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); + + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + .Where(l => userId == 0 || userId == l.Id) + .Select(u => u.Id); + + if (!await _licenseService.HasActiveLicense()) return; + + foreach (var uId in userIds) + { + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + foreach (var wtr in wantToRead) + { + if (!libAllowsScrobbling[wtr.LibraryId]) continue; + await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + } + + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + foreach (var rating in ratings) + { + if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + } + + var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); + foreach (var review in reviews) + { + if (!libAllowsScrobbling[review.Series.LibraryId]) continue; + await ScrobbleReviewUpdate(uId, review.SeriesId, review.Tagline, review.Review); + } + + var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, uId, + new UserParams(), new FilterDto() + { + ReadStatus = new ReadStatus() + { + Read = true, + InProgress = true, + NotRead = false + }, + Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() + }); + + foreach (var series in seriesWithProgress) + { + if (!libAllowsScrobbling[series.LibraryId]) continue; + if (series.PagesRead <= 0) continue; // Since we only scrobble when things are higher, we can + await ScrobbleReadingUpdate(uId, series.Id); + } + + } + } + + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ClearProcessedEvents() + { + var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(7); + _unitOfWork.ScrobbleRepository.Remove(events); + await _unitOfWork.CommitAsync(); + } + + /// + /// This is a task that is ran on a fixed schedule (every few hours or every day) that clears out the scrobble event table + /// and offloads the data to the API server which performs the syncing to the providers. + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ProcessUpdatesSinceLastSync() + { + // Check how many scrobble events we have available then only do those. + _logger.LogInformation("Starting Scrobble Processing"); + var userRateLimits = new Dictionary(); + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + + var progressCounter = 0; + + var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .AsEnumerable() + .Where(l => l.AllowScrobbling) + .Select(l => l.Id) + .ToImmutableHashSet(); + + var errors = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) + .Where(e => e.Comment == "Unknown Series") + .Select(e => e.SeriesId) + .ToList(); + + + var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var reviewEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.Review)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var decisions = addToWantToRead + .GroupBy(item => new { item.SeriesId, item.AppUserId }) + .Select(group => new + { + group.Key.SeriesId, + UserId = group.Key.AppUserId, + Event = group.First(), + Decision = group.Count() - removeWantToRead + .Count(removeItem => removeItem.SeriesId == group.Key.SeriesId && removeItem.AppUserId == group.Key.AppUserId) + }) + .Where(d => d.Decision > 0) + .Select(d => d.Event) + .ToList(); + + // For all userIds, ensure that we can connect and have access + var usersToScrobble = readEvents.Select(r => r.AppUser) + .Concat(addToWantToRead.Select(r => r.AppUser)) + .Concat(removeWantToRead.Select(r => r.AppUser)) + .Concat(ratingEvents.Select(r => r.AppUser)) + .DistinctBy(u => u.Id) + .ToList(); + foreach (var user in usersToScrobble) + { + await SetAndCheckRateLimit(userRateLimits, user, license.Value); + } + + var totalProgress = readEvents.Count + addToWantToRead.Count + removeWantToRead.Count + ratingEvents.Count + decisions.Count + reviewEvents.Count; + + _logger.LogInformation("Found {TotalEvents} Scrobble Events", totalProgress); + try + { + // Recalculate the highest volume/chapter + foreach (var readEvt in readEvents) + { + readEvt.VolumeNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + readEvt.ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + _unitOfWork.ScrobbleRepository.Update(readEvt); + } + progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, async evt => new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + ScrobbleDateUtc = evt.LastModifiedUtc, + Year = evt.Series.Metadata.ReleaseYear, + StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), + LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), + }); + + progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Rating = evt.Rating, + Year = evt.Series.Metadata.ReleaseYear + })); + + progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Rating = evt.Rating, + Year = evt.Series.Metadata.ReleaseYear, + ReviewBody = evt.ReviewBody, + ReviewTitle = evt.ReviewTitle + })); + + progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, + totalProgress, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Year = evt.Series.Metadata.ReleaseYear + })); + } + catch (FlurlHttpException) + { + _logger.LogError("Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); + return; + } + + + await SaveToDb(progressCounter, true); + _logger.LogInformation("Scrobbling Events is complete"); + } + + + private async Task ProcessEvents(IEnumerable events, IDictionary userRateLimits, + int usersToScrobble, int progressCounter, int totalProgress, Func> createEvent) + { + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + foreach (var evt in events) + { + _logger.LogDebug("Processing Reading Events: {Count} / {Total}", progressCounter, totalProgress); + progressCounter++; + // Check if this media item can even be processed for this user + if (!DoesUserHaveProviderAndValid(evt)) + { + continue; + } + var count = await SetAndCheckRateLimit(userRateLimits, evt.AppUser, license.Value); + if (count == 0) + { + if (usersToScrobble == 1) break; + continue; + } + + try + { + var data = await createEvent(evt); + userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt); + evt.IsProcessed = true; + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + } + catch (FlurlHttpException) + { + // If a flurl exception occured, the API is likely down. Kill processing + throw; + } + catch (Exception) + { + /* Swallow as it's already been handled in PostScrobbleUpdate */ + } + await SaveToDb(progressCounter); + // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others + var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); + await Task.Delay(delay); + } + + await SaveToDb(progressCounter, true); + return progressCounter; + } + + private async Task SaveToDb(int progressCounter, bool force = false) + { + if (!force || progressCounter % 5 == 0) + { + _logger.LogDebug("Saving Progress"); + await _unitOfWork.CommitAsync(); + } + } + + private static bool DoesUserHaveProviderAndValid(ScrobbleEvent readEvent) + { + var userProviders = GetUserProviders(readEvent.AppUser); + if (readEvent.Series.Library.Type == LibraryType.Manga && MangaProviders.Intersect(userProviders).Any()) + { + return true; + } + + if (readEvent.Series.Library.Type == LibraryType.Comic && + ComicProviders.Intersect(userProviders).Any()) + { + return true; + } + + if (readEvent.Series.Library.Type == LibraryType.Book && + BookProviders.Intersect(userProviders).Any()) + { + return true; + } + + return false; + } + + private static IList GetUserProviders(AppUser appUser) + { + var providers = new List(); + if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); + return providers; } /// @@ -556,23 +803,17 @@ public class ScrobblingService : IScrobblingService foreach (var webLink in webLinks.Split(',')) { if (!webLink.StartsWith(website)) continue; - var tokens = webLink.Split(website)[1].Split('/'); var value = tokens[index]; - - if (typeof(T) == typeof(int?)) + if (typeof(T) == typeof(int)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; + if (int.TryParse(value, out var intValue)) + return (T)(object)intValue; } - else if (typeof(T) == typeof(int)) + else if (typeof(T) == typeof(long)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; - - return default; - } - else if (typeof(T) == typeof(long?)) - { - if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) return (T)(object)longValue; + if (long.TryParse(value, out var longValue)) + return (T)(object)longValue; } else if (typeof(T) == typeof(string)) { @@ -580,818 +821,11 @@ public class ScrobblingService : IScrobblingService } } - return default; - } - - /// - /// Generate a URL from a given ID and website - /// - /// Type of the ID (e.g., int, long, string) - /// The ID to embed in the URL - /// The base website URL - /// The generated URL or null if the website is not supported - public static string? GenerateUrl(T id, string website) - { - if (!WeblinkExtractionMap.ContainsKey(website)) - { - return null; // Unsupported website - } - - if (Equals(id, default(T))) - { - throw new ArgumentNullException(nameof(id), "ID cannot be null."); - } - - // Ensure the type of the ID matches supported types - if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(string)) - { - return $"{website}{id}"; - } - - throw new ArgumentException("Unsupported ID type. Supported types are int, long, and string.", nameof(id)); - } - - public static string CreateUrl(string url, long? id) - { - return id is null or 0 ? string.Empty : $"{url}{id}/"; - } - - #endregion - - /// - /// Returns false if, the series is on hold or Don't Match, or when the library has scrobbling disable or not eligible - /// - /// - /// - /// - /// - private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) - { - if (series.DontMatch) return true; - - if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) - { - _logger.LogInformation("Series {SeriesName} is on AppUserId {AppUserId}'s hold list. Not scrobbling", series.Name, userId); - return true; - } - - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); - if (library is not {AllowScrobbling: true} || !ExternalMetadataService.IsPlusEligible(library.Type)) return true; - - return false; - } - - /// - /// Returns the rate limit from the K+ api - /// - /// - /// - /// - private async Task GetRateLimit(string license, string aniListToken) - { - if (string.IsNullOrWhiteSpace(aniListToken)) return 0; - - try - { - return await _kavitaPlusApiService.GetRateLimit(license, aniListToken); - } - catch (Exception e) - { - _logger.LogError(e, "An error happened trying to get rate limit from Kavita+ API"); - } - - return 0; - } - - #region Scrobble process (Requests to K+) - - /// - /// Retrieve all events for which the series has not errored, then delete all current errors - /// - private async Task PrepareScrobbleContext() - { - var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .AsEnumerable() - .Where(l => l.AllowScrobbling) - .Select(l => l.Id) - .ToImmutableHashSet(); - - var erroredSeries = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) - .Where(e => e.Comment is "Unknown Series" or UnknownSeriesErrorMessage or AccessTokenErrorMessage) - .Select(e => e.SeriesId) - .ToList(); - - var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - - return new ScrobbleSyncContext - { - ReadEvents = readEvents, - RatingEvents = ratingEvents, - AddToWantToRead = addToWantToRead, - RemoveWantToRead = removeWantToRead, - Decisions = CalculateNetWantToReadDecisions(addToWantToRead, removeWantToRead), - RateLimits = [], - License = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value, - }; - } - - /// - /// Filters users who can scrobble, sets their rate limit and updates the - /// - /// - /// - private async Task PrepareUsersToScrobble(ScrobbleSyncContext ctx) - { - // For all userIds, ensure that we can connect and have access - var usersToScrobble = ctx.ReadEvents.Select(r => r.AppUser) - .Concat(ctx.AddToWantToRead.Select(r => r.AppUser)) - .Concat(ctx.RemoveWantToRead.Select(r => r.AppUser)) - .Concat(ctx.RatingEvents.Select(r => r.AppUser)) - .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) - .Where(user => user.UserPreferences.AniListScrobblingEnabled) - .DistinctBy(u => u.Id) - .ToList(); - - foreach (var user in usersToScrobble) - { - await SetAndCheckRateLimit(ctx.RateLimits, user, ctx.License); - } - - ctx.Users = usersToScrobble; - } - - /// - /// Cleans up any events that are due to bugs or legacy - /// - private async Task CleanupOldOrBuggedEvents() - { - try - { - var eventsWithoutAnilistToken = (await _unitOfWork.ScrobbleRepository.GetEvents()) - .Where(e => e is { IsProcessed: false, IsErrored: false }) - .Where(e => string.IsNullOrEmpty(e.AppUser.AniListAccessToken)); - - _unitOfWork.ScrobbleRepository.Remove(eventsWithoutAnilistToken); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when trying to delete old scrobble events when the user has no active token"); - } - } - - /// - /// This is a task that is run on a fixed schedule (every few hours or every day) that clears out the scrobble event table - /// and offloads the data to the API server which performs the syncing to the providers. - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ProcessUpdatesSinceLastSync() - { - var ctx = await PrepareScrobbleContext(); - if (ctx.TotalCount == 0) return; - - // Get all the applicable users to scrobble and set their rate limits - await PrepareUsersToScrobble(ctx); - - _logger.LogInformation("Scrobble Processing Details:" + - "\n Read Events: {ReadEventsCount}" + - "\n Want to Read Events: {WantToReadEventsCount}" + - "\n Rating Events: {RatingEventsCount}" + - "\n Users to Scrobble: {UsersToScrobbleCount}" + - "\n Total Events to Process: {TotalEvents}", - ctx.ReadEvents.Count, - ctx.Decisions.Count, - ctx.RatingEvents.Count, - ctx.Users.Count, - ctx.TotalCount); - - try - { - await ProcessReadEvents(ctx); - await ProcessRatingEvents(ctx); - await ProcessWantToReadRatingEvents(ctx); - } - catch (FlurlHttpException ex) - { - _logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); - return; - } - - - await SaveToDb(ctx.ProgressCounter, true); - _logger.LogInformation("Scrobbling Events is complete"); - - await CleanupOldOrBuggedEvents(); - } - - /// - /// Calculates the net want-to-read decisions by considering all events. - /// Returns events that represent the final state for each user/series pair. - /// - /// List of events for adding to want-to-read - /// List of events for removing from want-to-read - /// List of events that represent the final state (add or remove) - private static List CalculateNetWantToReadDecisions(List addEvents, List removeEvents) - { - // Create a dictionary to track the latest event for each user/series combination - var latestEvents = new Dictionary<(int SeriesId, int AppUserId), ScrobbleEvent>(); - - // Process all add events - foreach (var addEvent in addEvents) - { - var key = (addEvent.SeriesId, addEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && addEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = addEvent; - latestEvents[key] = value; - } - - // Process all remove events - foreach (var removeEvent in removeEvents) - { - var key = (removeEvent.SeriesId, removeEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && removeEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = removeEvent; - latestEvents[key] = value; - } - - // Return all events that represent the final state - return latestEvents.Values.ToList(); - } - - private async Task ProcessWantToReadRatingEvents(ScrobbleSyncContext ctx) - { - await ProcessEvents(ctx.Decisions, ctx, evt => Task.FromResult(new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Year = evt.Series.Metadata.ReleaseYear - })); - - // After decisions, we need to mark all the want to read and remove from want to read as completed - var processedDecisions = ctx.Decisions.Where(d => d.IsProcessed).ToList(); - if (processedDecisions.Count > 0) - { - foreach (var scrobbleEvent in processedDecisions) - { - scrobbleEvent.IsProcessed = true; - scrobbleEvent.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(scrobbleEvent); - } - await _unitOfWork.CommitAsync(); - } - } - - private async Task ProcessRatingEvents(ScrobbleSyncContext ctx) - { - await ProcessEvents(ctx.RatingEvents, ctx, evt => Task.FromResult(new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Rating = evt.Rating, - Year = evt.Series.Metadata.ReleaseYear - })); - } - - private async Task ProcessReadEvents(ScrobbleSyncContext ctx) - { - // Recalculate the highest volume/chapter - foreach (var readEvt in ctx.ReadEvents) - { - // Note: this causes skewing in the scrobble history because it makes it look like there are duplicate events - readEvt.VolumeNumber = - (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - readEvt.ChapterNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - _unitOfWork.ScrobbleRepository.Update(readEvt); - } - - await ProcessEvents(ctx.ReadEvents, ctx, async evt => new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - ScrobbleDateUtc = evt.LastModifiedUtc, - Year = evt.Series.Metadata.ReleaseYear, - StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), - LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), - }); - } - - /// - /// Returns true if the user token is valid - /// - /// - /// - /// If the token is not, adds a scrobble error - private async Task ValidateUserToken(ScrobbleEvent evt) - { - if (!TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) - return true; - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = "AniList token has expired and needs rotating. Scrobbling wont work until then", - Details = $"User: {evt.AppUser.UserName}, Expired: {TokenService.GetTokenExpiry(evt.AppUser.AniListAccessToken)}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - await _unitOfWork.CommitAsync(); - return false; - } - - /// - /// Returns true if the series can be scrobbled - /// - /// - /// - /// If the series cannot be scrobbled, adds a scrobble error - private async Task ValidateSeriesCanBeScrobbled(ScrobbleEvent evt) - { - if (evt.Series is { IsBlacklisted: false, DontMatch: false }) - return true; - - _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", - evt.Series.Name, evt.SeriesId); - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = UnknownSeriesErrorMessage, - Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - - evt.SetErrorMessage(UnknownSeriesErrorMessage); - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - await _unitOfWork.CommitAsync(); - return false; - } - - /// - /// Removed Special parses numbers from chatter and volume numbers - /// - /// - /// - private static ScrobbleDto NormalizeScrobbleData(ScrobbleDto data) - { - // We need to handle the encoding and changing it to the old one until we can update the API layer to handle these - // which could happen in v0.8.3 - if (data.VolumeNumber is Parser.SpecialVolumeNumber or Parser.DefaultChapterNumber) - { - data.VolumeNumber = 0; - } - - - if (data.ChapterNumber is Parser.DefaultChapterNumber) - { - data.ChapterNumber = 0; - } - - - return data; - } - - /// - /// Loops through all events, and post them to K+ - /// - /// - /// - /// - private async Task ProcessEvents(IEnumerable events, ScrobbleSyncContext ctx, Func> createEvent) - { - foreach (var evt in events.Where(CanProcessScrobbleEvent)) - { - _logger.LogDebug("Processing Scrobble Events: {Count} / {Total}", ctx.ProgressCounter, ctx.TotalCount); - ctx.ProgressCounter++; - - if (!await ValidateUserToken(evt)) continue; - if (!await ValidateSeriesCanBeScrobbled(evt)) continue; - - var count = await SetAndCheckRateLimit(ctx.RateLimits, evt.AppUser, ctx.License); - if (count == 0) - { - if (ctx.Users.Count == 1) break; - continue; - } - - try - { - var data = NormalizeScrobbleData(await createEvent(evt)); - - ctx.RateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, ctx.License, evt); - - evt.IsProcessed = true; - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - } - catch (FlurlHttpException) - { - // If a flurl exception occured, the API is likely down. Kill processing - throw; - } - catch (KavitaException ex) - { - if (ex.Message.Contains("Access token is invalid")) - { - _logger.LogCritical(ex, "Access Token for AppUserId: {AppUserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); - evt.SetErrorMessage(AccessTokenErrorMessage); - _unitOfWork.ScrobbleRepository.Update(evt); - - // Ensure series with this error do not get re-processed next sync - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = AccessTokenErrorMessage, - Details = $"{evt.AppUser.UserName} has an invalid access token (K+ Error)", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId, - }); - } - } - catch (Exception ex) - { - /* Swallow as it's already been handled in PostScrobbleUpdate */ - _logger.LogError(ex, "Error processing event {EventId}", evt.Id); - } - - await SaveToDb(ctx.ProgressCounter); - - // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others - var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); - await Task.Delay(delay); - } - - await SaveToDb(ctx.ProgressCounter, true); - } - - /// - /// Save changes every five updates - /// - /// - /// Ignore update count check - private async Task SaveToDb(int progressCounter, bool force = false) - { - if ((force || progressCounter % 5 == 0) && _unitOfWork.HasChanges()) - { - _logger.LogDebug("Saving Scrobbling Event Processing Progress"); - await _unitOfWork.CommitAsync(); - } - } - - /// - /// If no errors have been logged for the given series, creates a new Unknown series error, and blacklists the series - /// - /// - /// - private async Task MarkSeriesAsUnknown(ScrobbleDto data, ScrobbleEvent evt) - { - if (await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) return; - - // Create a new ExternalMetadata entry to indicate that this is not matchable - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata); - if (series == null) return; - - series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata {SeriesId = evt.SeriesId}; - series.IsBlacklisted = true; - _unitOfWork.SeriesRepository.Update(series); - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - - /// - /// Makes the K+ request, and handles any exceptions that occur - /// - /// Data to send to K+ - /// K+ license key - /// Related scrobble event - /// - /// Exceptions may be rethrown as a KavitaException - /// Some FlurlHttpException are also rethrown - public async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) - { - try - { - var response = await _kavitaPlusApiService.PostScrobbleUpdate(data, license); - - _logger.LogDebug("K+ API Scrobble response for series {SeriesName}: Successful {Successful}, ErrorMessage {ErrorMessage}, ExtraInformation: {ExtraInformation}, RateLeft: {RateLeft}", - data.SeriesName, response.Successful, response.ErrorMessage, response.ExtraInformation, response.RateLeft); - - if (response.Successful || response.ErrorMessage == null) return response.RateLeft; - - // Might want to log this under ScrobbleError - if (response.ErrorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - - if (response.ErrorMessage.Contains("Unauthorized")) - { - _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); - await _licenseService.HasActiveLicense(true); - evt.SetErrorMessage(InvalidKPlusLicenseErrorMessage); - throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); - } - - if (response.ErrorMessage.Contains("Access token is invalid")) - { - evt.SetErrorMessage(AccessTokenErrorMessage); - throw new KavitaException("Access token is invalid"); - } - - if (response.ErrorMessage.Contains("Unknown Series")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to match the series: {SeriesName}", evt.Series.Name); - await MarkSeriesAsUnknown(data, evt); - evt.SetErrorMessage(UnknownSeriesErrorMessage); - } else if (response.ErrorMessage.StartsWith("Review")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to save the review"); - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = response.ErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.SetErrorMessage(ReviewFailedErrorMessage); - } - - return response.RateLeft; - } - catch (FlurlHttpException ex) - { - var errorMessage = await ex.GetResponseStringAsync(); - // Trim quotes if the response is a JSON string - errorMessage = errorMessage.Trim('"'); - - if (errorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - - _logger.LogError(ex, "Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); - if (ex.StatusCode == 500 || ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) - { - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.SetErrorMessage(BadPayLoadErrorMessage); - throw new KavitaException(BadPayLoadErrorMessage); - } - throw; - } - } - - #endregion - - #region BackFill - - - /// - /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license - /// - /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user - public async Task CreateEventsFromExistingHistory(int userId = 0) - { - if (!await _licenseService.HasActiveLicense()) return; - - if (userId != 0) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; - if (user.HasRunScrobbleEventGeneration) - { - _logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName); - return; - } - } - - var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) - .Where(l => userId == 0 || userId == l.Id) - .Where(u => !u.HasRunScrobbleEventGeneration) - .Select(u => u.Id); - - foreach (var uId in userIds) - { - await CreateEventsFromExistingHistoryForUser(uId, libAllowsScrobbling); - } - } - - /// - /// Creates wantToRead, rating, reviews, and series progress events for the suer - /// - /// - /// - private async Task CreateEventsFromExistingHistoryForUser(int userId, Dictionary libAllowsScrobbling) - { - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(userId); - foreach (var wtr in wantToRead) - { - if (!libAllowsScrobbling[wtr.LibraryId]) continue; - await ScrobbleWantToReadUpdate(userId, wtr.Id, true); - } - - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(userId); - foreach (var rating in ratings) - { - if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; - await ScrobbleRatingUpdate(userId, rating.SeriesId, rating.Rating); - } - - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(userId); - foreach (var review in reviews.Where(r => !string.IsNullOrEmpty(r.Review))) - { - if (!libAllowsScrobbling[review.Series.LibraryId]) continue; - await ScrobbleReviewUpdate(userId, review.SeriesId, string.Empty, review.Review!); - } - - var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, userId, - new UserParams(), new FilterDto - { - ReadStatus = new ReadStatus - { - Read = true, - InProgress = true, - NotRead = false - }, - Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() - }); - - foreach (var series in seriesWithProgress.Where(series => series.PagesRead > 0)) - { - if (!libAllowsScrobbling[series.LibraryId]) continue; - await ScrobbleReadingUpdate(userId, series.Id); - } - - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user != null) - { - user.HasRunScrobbleEventGeneration = true; - user.ScrobbleEventGenerationRan = DateTime.UtcNow; - await _unitOfWork.CommitAsync(); - } - } - - public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) - { - if (!await _licenseService.HasActiveLicense()) return; - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); - if (series == null || !series.Library.AllowScrobbling) return; - - _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.Id); - - foreach (var uId in userIds) - { - // Handle "Want to Read" updates specific to the series - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); - foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) - { - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); - } - - // Handle ratings specific to the series - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); - foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) - { - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); - } - - // Handle review specific to the series - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); - foreach (var review in reviews.Where(r => r.SeriesId == seriesId && !string.IsNullOrEmpty(r.Review))) - { - await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!); - } - - // Handle progress updates for the specific series - await ScrobbleReadingUpdate(uId, seriesId); - } - } - - #endregion - - /// - /// Removes all events (active) that are tied to a now-on hold series - /// - /// - /// - public async Task ClearEventsForSeries(int userId, int seriesId) - { - _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); - - var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); - } - - /// - /// Removes all events that have been processed that are 7 days old - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ClearProcessedEvents() - { - const int daysAgo = 7; - var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); - _unitOfWork.ScrobbleRepository.Remove(events); - _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); - await _unitOfWork.CommitAsync(); - } - - private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) - { - var userProviders = GetUserProviders(readEvent.AppUser); - switch (readEvent.Series.Library.Type) - { - case LibraryType.Manga when MangaProviders.Intersect(userProviders).Any(): - case LibraryType.Comic when ComicProviders.Intersect(userProviders).Any(): - case LibraryType.Book when BookProviders.Intersect(userProviders).Any(): - case LibraryType.LightNovel when LightNovelProviders.Intersect(userProviders).Any(): - return true; - default: - return false; - } - } - - private static List GetUserProviders(AppUser appUser) - { - var providers = new List(); - if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); - - return providers; + return default(T?); } private async Task SetAndCheckRateLimit(IDictionary userRateLimits, AppUser user, string license) { - if (string.IsNullOrEmpty(user.AniListAccessToken)) return 0; try { if (!userRateLimits.ContainsKey(user.Id)) @@ -1402,7 +836,7 @@ public class ScrobblingService : IScrobblingService } catch (Exception ex) { - _logger.LogInformation(ex, "User {UserName} had an issue figuring out rate: {Message}", user.UserName, ex.Message); + _logger.LogInformation("User {UserName} had an issue figuring out rate: {Message}", user.UserName, ex.Message); userRateLimits.Add(user.Id, 0); } @@ -1415,4 +849,9 @@ public class ScrobblingService : IScrobblingService return count; } + public static string CreateUrl(string url, long? id) + { + if (id is null or 0) return string.Empty; + return $"{url}{id}/"; + } } diff --git a/API/Services/Plus/SmartCollectionSyncService.cs b/API/Services/Plus/SmartCollectionSyncService.cs deleted file mode 100644 index 1bd0dfb6b..000000000 --- a/API/Services/Plus/SmartCollectionSyncService.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.SignalR; -using Flurl.Http; -using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using Microsoft.Extensions.Logging; - -namespace API.Services.Plus; -#nullable enable - -internal sealed class SeriesCollection -{ - public required IList Series { get; set; } - public required string Summary { get; set; } - public required string Title { get; set; } - /// - /// Total items in the source, not what was matched - /// - public int TotalItems { get; set; } -} - -/// -/// Responsible to synchronize Collection series from non-Kavita sources -/// -public interface ISmartCollectionSyncService -{ - /// - /// Synchronize all collections - /// - /// - Task Sync(); - /// - /// Synchronize a collection - /// - /// - /// - Task Sync(int collectionId); -} - -public class SmartCollectionSyncService : ISmartCollectionSyncService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly ILicenseService _licenseService; - - private const int SyncDelta = -2; - // Allow 50 requests per 24 hours - private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false); - - - public SmartCollectionSyncService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _licenseService = licenseService; - } - - /// - /// For every Sync-eligible collection, synchronize with upstream - /// - /// - public async Task Sync() - { - if (!await _licenseService.HasActiveLicense()) return; - var expirationTime = DateTime.UtcNow.AddDays(SyncDelta).Truncate(TimeSpan.TicksPerHour); - var collections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsForSyncing(expirationTime)) - .Where(CanSync) - .ToList(); - - _logger.LogInformation("Found {Count} collections to synchronize", collections.Count); - foreach (var collection in collections) - { - try - { - await SyncCollection(collection); - } - catch (RateLimitException) - { - break; - } - } - - _logger.LogInformation("Synchronization complete"); - } - - public async Task Sync(int collectionId) - { - if (!await _licenseService.HasActiveLicense()) return; - var collection = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, CollectionIncludes.Series); - if (!CanSync(collection)) - { - _logger.LogInformation("Requested to sync {CollectionName} but not applicable to sync", collection!.Title); - return; - } - - try - { - await SyncCollection(collection!); - } catch (RateLimitException) {/* Swallow */} - } - - private static bool CanSync(AppUserCollection? collection) - { - if (collection is not {Source: ScrobbleProvider.Mal}) return false; - if (string.IsNullOrEmpty(collection.SourceUrl)) return false; - if (collection.LastSyncUtc.Truncate(TimeSpan.TicksPerHour) >= DateTime.UtcNow.AddDays(SyncDelta).Truncate(TimeSpan.TicksPerHour)) return false; - return true; - } - - private async Task SyncCollection(AppUserCollection collection) - { - if (!RateLimiter.TryAcquire(string.Empty)) - { - // Request not allowed due to rate limit - _logger.LogDebug("Rate Limit hit for Smart Collection Sync"); - throw new RateLimitException(); - } - - var info = await GetStackInfo(GetStackId(collection.SourceUrl!)); - if (info == null) - { - _logger.LogInformation("Unable to find collection through Kavita+"); - return; - } - - // Check each series in the collection against what's in the target - // For everything that's not there, link it up for this user. - _logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems); - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started)); - - var missingCount = 0; - var missingSeries = new StringBuilder(); - var counter = -1; - foreach (var seriesInfo in info.Series.OrderBy(s => s.SeriesName)) - { - counter++; - try - { - // Normalize series name and localized name - var normalizedSeriesName = seriesInfo.SeriesName?.ToNormalized(); - var normalizedLocalizedSeriesName = seriesInfo.LocalizedSeriesName?.ToNormalized(); - - // Search for existing series in the collection - var formats = seriesInfo.PlusMediaFormat.GetMangaFormats(); - var existingSeries = collection.Items.FirstOrDefault(s => - (s.Name.ToNormalized() == normalizedSeriesName || - s.NormalizedName == normalizedSeriesName || - s.LocalizedName.ToNormalized() == normalizedLocalizedSeriesName || - s.NormalizedLocalizedName == normalizedLocalizedSeriesName || - - s.NormalizedName == normalizedLocalizedSeriesName || - s.NormalizedLocalizedName == normalizedSeriesName) - && formats.Contains(s.Format)); - - _logger.LogDebug("Trying to find {SeriesName} with formats ({Formats}) within Kavita for linking. Found: {ExistingSeriesName} ({ExistingSeriesId})", - seriesInfo.SeriesName, formats, existingSeries?.Name, existingSeries?.Id); - - if (existingSeries != null) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); - continue; - } - - // Series not found in the collection, try to find it in the server - var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName, - seriesInfo.LocalizedSeriesName, - formats, collection.AppUserId); - - collection.Items ??= new List(); - if (newSeries != null) - { - // Add the new series to the collection - collection.Items.Add(newSeries); - - } - else - { - _logger.LogDebug("{Series} not found in the server", seriesInfo.SeriesName); - missingCount++; - missingSeries.Append( - $"{seriesInfo.SeriesName}"); - missingSeries.Append("
"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An exception occured when linking up a series to the collection. Skipping"); - missingCount++; - missingSeries.Append( - $"{seriesInfo.SeriesName}"); - missingSeries.Append("
"); - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); - } - - // At this point, all series in the info have been checked and added if necessary - // You may want to commit changes to the database if needed - collection.LastSyncUtc = DateTime.UtcNow.Truncate(TimeSpan.TicksPerHour); - collection.TotalSourceCount = info.TotalItems; - collection.Summary = info.Summary; - collection.MissingSeriesFromSource = missingSeries.ToString(); - - _unitOfWork.CollectionTagRepository.Update(collection); - - try - { - await _unitOfWork.CommitAsync(); - - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection); - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended)); - - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, - MessageFactory.CollectionUpdatedEvent(collection.Id), false); - - _logger.LogInformation("Finished Syncing Collection {CollectionName} - Missing {MissingCount} series", - collection.Title, missingCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error during saving the collection"); - } - } - - - - private static long GetStackId(string url) - { - var tokens = url.Split("/"); - return long.Parse(tokens[^1], CultureInfo.InvariantCulture); - } - - private async Task GetStackInfo(long stackId) - { - _logger.LogDebug("Fetching Kavita+ for MAL Stack"); - - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - - var seriesForStack = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stack?stackId=" + stackId) - .WithKavitaPlusHeaders(license) - .GetJsonAsync(); - - return seriesForStack; - } -} diff --git a/API/Services/Plus/WantToReadSyncService.cs b/API/Services/Plus/WantToReadSyncService.cs deleted file mode 100644 index a6d536911..000000000 --- a/API/Services/Plus/WantToReadSyncService.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using Flurl.Http; -using Hangfire; -using Kavita.Common; -using Microsoft.Extensions.Logging; -using Org.BouncyCastle.Bcpg.Sig; - -namespace API.Services.Plus; - - -public interface IWantToReadSyncService -{ - Task Sync(); -} - -/// -/// Responsible for syncing Want To Read from upstream providers with Kavita -/// -public class WantToReadSyncService : IWantToReadSyncService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly ILicenseService _licenseService; - - public WantToReadSyncService(IUnitOfWork unitOfWork, ILogger logger, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _licenseService = licenseService; - } - - public async Task Sync() - { - if (!await _licenseService.HasActiveLicense()) return; - - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead | AppUserIncludes.UserPreferences); - foreach (var user in users.Where(u => u.UserPreferences.WantToReadSync)) - { - if (string.IsNullOrEmpty(user.MalUserName) && string.IsNullOrEmpty(user.AniListAccessToken)) continue; - - try - { - _logger.LogInformation("Syncing want to read for user: {UserName}", user.UserName); - var wantToReadSeries = - await ( - $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/want-to-read?malUsername={user.MalUserName}&aniListToken={user.AniListAccessToken}") - .WithKavitaPlusHeaders(license) - .WithTimeout( - TimeSpan.FromSeconds(120)) // Give extra time as MAL + AniList can result in a lot of data - .GetJsonAsync>(); - - // Match the series (note: There may be duplicates in the final result) - foreach (var unmatchedSeries in wantToReadSeries) - { - var match = await _unitOfWork.SeriesRepository.MatchSeries(unmatchedSeries); - if (match == null) - { - continue; - } - - // There is a match, add it - user.WantToRead.Add(new AppUserWantToRead() - { - SeriesId = match.Id, - }); - _logger.LogDebug("Added {MatchName} ({Format}) to Want to Read", match.Name, match.Format); - } - - // Remove existing Want to Read that are duplicates - user.WantToRead = user.WantToRead.DistinctBy(d => d.SeriesId).ToList(); - - // TODO: Need to write in the history table the last sync time - - // Save the left over entities - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - // Trigger CleanupService to cleanup any series in WantToRead that don't belong - RecurringJob.TriggerJob(TaskScheduler.RemoveFromWantToReadTaskId); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when processing want to read series sync for {User}", user.UserName); - } - } - - } - - // Allow syncing if there are any libraries that have an appropriate Provider, the user has the appropriate token, and the last Sync validates - // private async Task CanSync(AppUser? user) - // { - // - // if (collection is not {Source: ScrobbleProvider.Mal}) return false; - // if (string.IsNullOrEmpty(collection.SourceUrl)) return false; - // if (collection.LastSyncUtc.Truncate(TimeSpan.TicksPerHour) >= DateTime.UtcNow.AddDays(SyncDelta).Truncate(TimeSpan.TicksPerHour)) return false; - // return true; - // } -} diff --git a/API/Services/RatingService.cs b/API/Services/RatingService.cs deleted file mode 100644 index ccaebba69..000000000 --- a/API/Services/RatingService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Services.Plus; -using Hangfire; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IRatingService -{ - /// - /// Updates the users' rating for a given series - /// - /// Should include ratings - /// - /// - Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto); - - /// - /// Updates the users' rating for a given chapter - /// - /// Should include ratings - /// chapterId must be set - /// - Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto); -} - -public class RatingService: IRatingService -{ - - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILogger _logger; - - public RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _logger = logger; - } - - public async Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto) - { - var userRating = - await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ?? - new AppUserRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - - if (userRating.Id == 0) - { - user.Ratings ??= new List(); - user.Ratings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) - { - BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, - userRating.Rating)); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.Ratings?.Remove(userRating); - - return false; - } - - public async Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto) - { - if (updateRatingDto.ChapterId == null) - { - return false; - } - - var userRating = - await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value) ?? - new AppUserChapterRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - userRating.ChapterId = updateRatingDto.ChapterId.Value; - - if (userRating.Id == 0) - { - user.ChapterRatings ??= new List(); - user.ChapterRatings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.ChapterRatings?.Remove(userRating); - - return false; - } - -} diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 3b3cb37d5..a56981069 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -9,7 +9,6 @@ using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.Progress; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; @@ -23,7 +22,6 @@ using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable public interface IReaderService { @@ -52,9 +50,8 @@ public class ReaderService : IReaderService private readonly IImageService _imageService; private readonly IDirectoryService _directoryService; private readonly IScrobblingService _scrobblingService; - private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default; - private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default; - private readonly ChapterSortComparerSpecialsLast _chapterSortComparerSpecialsLast = ChapterSortComparerSpecialsLast.Default; + private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default; + private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default; private const float MinWordsPerHour = 10260F; private const float MaxWordsPerHour = 30000F; @@ -77,7 +74,7 @@ public class ReaderService : IReaderService public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId) { - return Parser.NormalizePath(Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}")); + return Tasks.Scanner.Parser.Parser.NormalizePath(Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}")); } /// @@ -122,7 +119,6 @@ public class ReaderService : IReaderService var seenVolume = new Dictionary(); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) throw new KavitaException("series-doesnt-exist"); - foreach (var chapter in chapters) { var userProgress = GetUserProgressForChapter(user, chapter); @@ -135,7 +131,7 @@ public class ReaderService : IReaderService VolumeId = chapter.VolumeId, SeriesId = seriesId, ChapterId = chapter.Id, - LibraryId = series.LibraryId, + LibraryId = series.LibraryId }); } else @@ -145,14 +141,13 @@ public class ReaderService : IReaderService userProgress.VolumeId = chapter.VolumeId; } - userProgress?.MarkModified(); - await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages)); // Send out volume events for each distinct volume - if (seenVolume.TryAdd(chapter.VolumeId, true)) + if (!seenVolume.ContainsKey(chapter.VolumeId)) { + seenVolume[chapter.VolumeId] = true; await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, 0, chapters.Where(c => c.VolumeId == chapter.VolumeId).Sum(c => c.Pages))); @@ -180,7 +175,6 @@ public class ReaderService : IReaderService userProgress.PagesRead = 0; userProgress.SeriesId = seriesId; userProgress.VolumeId = chapter.VolumeId; - userProgress.MarkModified(); await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0)); @@ -203,7 +197,7 @@ public class ReaderService : IReaderService /// Must have Progresses populated /// /// - private AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter) + private static AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter) { AppUserProgress? userProgress = null; @@ -223,12 +217,11 @@ public class ReaderService : IReaderService var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList(); if (progresses.Count > 1) { - var highestProgress = progresses.Max(x => x.PagesRead); - var firstProgress = progresses.OrderBy(p => p.LastModifiedUtc).First(); - firstProgress.PagesRead = highestProgress; - user.Progresses = [firstProgress]; + user.Progresses = new List + { + user.Progresses.First() + }; userProgress = user.Progresses.First(); - _logger.LogInformation("Trying to save progress and multiple progress entries exist, deleting and rewriting with highest progress rate: {@Progress}", userProgress); } } @@ -271,7 +264,7 @@ public class ReaderService : IReaderService SeriesId = progressDto.SeriesId, ChapterId = progressDto.ChapterId, LibraryId = progressDto.LibraryId, - BookScrollId = progressDto.BookScrollId, + BookScrollId = progressDto.BookScrollId }); _unitOfWork.UserRepository.Update(userWithProgress); } @@ -285,9 +278,6 @@ public class ReaderService : IReaderService _unitOfWork.AppUserProgressRepository.Update(userProgress); } - _logger.LogDebug("Saving Progress on Chapter {ChapterId} from Series {SeriesId} to {PageNum}", progressDto.ChapterId, progressDto.SeriesId, progressDto.PageNum); - userProgress?.MarkModified(); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); @@ -356,23 +346,11 @@ public class ReaderService : IReaderService return page; } - private int GetNextSpecialChapter(VolumeDto volume, ChapterDto currentChapter) - { - if (volume.IsSpecial()) - { - // Handle specials by sorting on their Filename aka Range - return GetNextChapterId(volume.Chapters.OrderBy(x => x.SortOrder), currentChapter.SortOrder, dto => dto.SortOrder); - } - - return -1; - } - - /// /// Tries to find the next logical Chapter /// /// - /// V1 → V2 → V3 chapter 0 → V3 chapter 10 → V0 chapter 1 -> V0 chapter 2 -> (Annual 1 -> Annual 2) -> (SP 01 → SP 02) + /// V1 → V2 → V3 chapter 0 → V3 chapter 10 → V0 chapter 1 -> V0 chapter 2 -> SP 01 → SP 02 /// /// /// @@ -381,88 +359,113 @@ public class ReaderService : IReaderService /// -1 if nothing can be found public async Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId) { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId); + var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) + .ToList(); + var currentVolume = volumes.Single(v => v.Id == volumeId); + var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - var currentVolume = volumes.FirstOrDefault(v => v.Id == volumeId); - if (currentVolume == null) + if (currentVolume.Number == 0) { - // Handle the case where the current volume is not found - return -1; - } - - var currentChapter = currentVolume.Chapters.FirstOrDefault(c => c.Id == currentChapterId); - if (currentChapter == null) - { - // Handle the case where the current chapter is not found - return -1; - } - - var currentVolumeIndex = volumes.IndexOf(currentVolume); - var chapterId = -1; - - if (currentVolume.IsSpecial()) - { - // Handle specials by sorting on their Range - chapterId = GetNextSpecialChapter(currentVolume, currentChapter); - return chapterId; - } - - if (currentVolume.IsLooseLeaf()) - { - // Handle loose-leaf chapters - chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder), - currentChapter.SortOrder, - dto => dto.SortOrder); + // Handle specials by sorting on their Filename aka Range + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; - - // Check specials next, as that is the order - if (currentVolumeIndex + 1 >= volumes.Count) return -1; // There are no special volumes, so there is nothing - - var specialVolume = volumes[currentVolumeIndex + 1]; - if (!specialVolume.IsSpecial()) return -1; - return specialVolume.Chapters.OrderByNatural(c => c.Range).FirstOrDefault()?.Id ?? -1; } - // Check within the current volume if the next chapter within it can be next - var chapters = currentVolume.Chapters.OrderBy(c => c.MinNumber).ToList(); - var currentChapterIndex = chapters.IndexOf(currentChapter); - if (currentChapterIndex < chapters.Count - 1) + var currentVolumeNumber = currentVolume.Name.AsFloat(); + var next = false; + foreach (var volume in volumes) { - return chapters[currentChapterIndex + 1].Id; - } - - // Check within the current Volume - chapterId = GetNextChapterId(chapters, currentChapter.SortOrder, dto => dto.SortOrder); - if (chapterId > 0) return chapterId; - - // Now check the next volume - var nextVolumeIndex = currentVolumeIndex + 1; - if (nextVolumeIndex < volumes.Count) - { - // Get the first chapter from the next volume - chapterId = volumes[nextVolumeIndex].Chapters.MinBy(c => c.MinNumber, _chapterSortComparerForInChapterSorting)?.Id ?? -1; - return chapterId; - } - - // We are the last volume, so we need to check loose leaf - if (currentVolumeIndex == volumes.Count - 1) - { - // Try to find the first loose-leaf chapter in this volume - var firstLooseLeafChapter = volumes.WhereLooseLeaf().FirstOrDefault()?.Chapters.MinBy(c => c.MinNumber, _chapterSortComparerForInChapterSorting); - if (firstLooseLeafChapter != null) + var volumeNumbersMatch = Math.Abs(volume.Name.AsFloat() - currentVolumeNumber) < 0.00001f; + if (volumeNumbersMatch && volume.Chapters.Count > 1) { - return firstLooseLeafChapter.Id; + // Handle Chapters within current Volume + // In this case, i need 0 first because 0 represents a full volume file. + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsFloat(), _chapterSortComparer), + currentChapter.Range, dto => dto.Range); + if (chapterId > 0) return chapterId; + next = true; + continue; } + + if (volumeNumbersMatch) + { + next = true; + continue; + } + + if (!next) continue; + + // Handle Chapters within next Volume + // ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+ + var chapters = volume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer).ToList(); + if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters.Last().Number.Equals(Parser.DefaultChapter)) + { + // We need to handle an extra check if the current chapter is the last special, as we should return -1 + if (currentChapter.IsSpecial) return -1; + + return chapters.Last().Id; + } + + var firstChapter = chapters.FirstOrDefault(); + if (firstChapter == null) break; + var isSpecial = firstChapter.IsSpecial || currentChapter.IsSpecial; + if (isSpecial) + { + var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number), + currentChapter.Range, dto => dto.Range); + if (chapterId > 0) return chapterId; + } else if (firstChapter.Number.AsDouble() >= currentChapter.Number.AsDouble()) return firstChapter.Id; + // If we are the last chapter and next volume is there, we should try to use it (unless it's volume 0) + else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id; + + // If on last volume AND there are no specials left, then let's return -1 + var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + .SelectMany(v => v.Chapters.Where(c => c.IsSpecial)).Any(); + if (currentVolume.Number != 0 && !anySpecials) + { + return -1; + } + } + + + + // If we are the last volume and we didn't find any next volume, loop back to volume 0 and give the first chapter + // This has an added problem that it will loop up to the beginning always + // Should I change this to Max number? volumes.LastOrDefault()?.Number -> volumes.Max(v => v.Number) + + if (currentVolume.Number != 0 && currentVolume.Number == volumes.LastOrDefault()?.Number && volumes.Count > 1) + { + var chapterVolume = volumes.FirstOrDefault(); + if (chapterVolume?.Number != 0) return -1; + + // This is my attempt at fixing a bug where we loop around to the beginning, but I just can't seem to figure it out + // var orderedVolumes = volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).ToList(); + // if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number) + // { + // // We can move into loose leaf chapters + // //var firstLooseLeaf = volumes.LastOrDefault().Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer); + // var nextChapterId = GetNextChapterId( + // volumes.LastOrDefault().Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer), + // "0", dto => dto.Range); + // // CHECK if we need a IsSpecial check + // if (nextChapterId > 0) return nextChapterId; + // } + + + var firstChapter = chapterVolume.Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer); + if (firstChapter == null) return -1; + + + return firstChapter.Id; } return -1; } - /// /// Tries to find the prev logical Chapter /// /// - /// V1 ← V2 ← V3 chapter 0 ← V3 chapter 10 ← (V0 chapter 1 ← V0 chapter 2 ← SP 01 ← SP 02) + /// V1 ← V2 ← V3 chapter 0 ← V3 chapter 10 ← V0 chapter 1 ← V0 chapter 2 ← SP 01 ← SP 02 /// /// /// @@ -471,76 +474,52 @@ public class ReaderService : IReaderService /// -1 if nothing can be found public async Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId) { - var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList(); + var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).Reverse().ToList(); var currentVolume = volumes.Single(v => v.Id == volumeId); var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - var chapterId = -1; - - if (currentVolume.IsSpecial()) + if (currentVolume.Number == 0) { - // Check within Specials, if not set the currentVolume to Loose Leaf - chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder).Reverse(), - currentChapter.SortOrder, - dto => dto.SortOrder); + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Range, + dto => dto.Range); if (chapterId > 0) return chapterId; - currentVolume = volumes.Find(v => v.IsLooseLeaf()); } - if (currentVolume != null && currentVolume.IsLooseLeaf()) + var next = false; + foreach (var volume in volumes) { - // If loose leaf, handle within the loose leaf. If not there, then set currentVolume to volumes.Last() where not LooseLeaf or Special - var currentVolumeChapters = currentVolume.Chapters.OrderBy(x => x.SortOrder).ToList(); - chapterId = GetPrevChapterId(currentVolumeChapters, - currentChapter.SortOrder, dto => dto.SortOrder, c => c.Id); - if (chapterId > 0) return chapterId; - currentVolume = volumes.FindLast(v => !v.IsLooseLeaf() && !v.IsSpecial()); - if (currentVolume != null) return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1; + if (volume.Number == currentVolume.Number) + { + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(), + currentChapter.Range, dto => dto.Range); + if (chapterId > 0) return chapterId; + next = true; // When the diff between volumes is more than 1, we need to explicitly tell that next volume is our use case + continue; + } + if (next) + { + if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work + var lastChapter = volume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); + if (lastChapter == null) return -1; + return lastChapter.Id; + } } - // When we started as a special and there was no loose leafs, reset the currentVolume - if (currentVolume == null) + var lastVolume = volumes.MaxBy(v => v.Number); + if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1) { - currentVolume = volumes.Find(v => !v.IsLooseLeaf() && !v.IsSpecial()); - if (currentVolume == null) return -1; - return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1; + var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); + if (lastChapter == null) return -1; + return lastChapter.Id; } - // At this point, only need to check within the current Volume else move 1 level back - // Check current volume - chapterId = GetPrevChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder), - currentChapter.SortOrder, dto => dto.SortOrder, c => c.Id); - if (chapterId > 0) return chapterId; - - - var currentVolumeIndex = volumes.IndexOf(currentVolume); - if (currentVolumeIndex == 0) return -1; - currentVolume = volumes[currentVolumeIndex - 1]; - if (currentVolume.IsLooseLeaf() || currentVolume.IsSpecial()) return -1; - chapterId = currentVolume.Chapters.OrderBy(x => x.SortOrder).Last().Id; - if (chapterId > 0) return chapterId; - - return -1; - } - - private static int GetPrevChapterId(IEnumerable source, float currentValue, Func selector, Func idSelector) - { - var sortedSource = source.OrderBy(selector).ToList(); - var currentChapterIndex = sortedSource.FindIndex(x => selector(x).Is(currentValue)); - - if (currentChapterIndex > 0) - { - return idSelector(sortedSource[currentChapterIndex - 1]); - } - - // There is no previous chapter return -1; } /// /// Finds the chapter to continue reading from. If a chapter has progress and not complete, return that. If not, progress in the - /// ordering (Volumes -> Loose Chapters -> Annuals -> Special) to find next chapter. If all are read, return first in order for series. + /// ordering (Volumes -> Loose Chapters -> Special) to find next chapter. If all are read, return first in order for series. /// /// /// @@ -549,59 +528,29 @@ public class ReaderService : IReaderService { var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList(); - var anyUserProgress = - await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId); - - if (!anyUserProgress) - { - // I think i need a way to sort volumes last - volumes = volumes.OrderBy(v => v.MinNumber, _chapterSortComparerSpecialsLast).ToList(); - - // Check if we have a non-loose leaf volume - var nonLooseLeafNonSpecialVolume = volumes.Find(v => !v.IsLooseLeaf() && !v.IsSpecial()); - if (nonLooseLeafNonSpecialVolume != null) - { - return nonLooseLeafNonSpecialVolume.Chapters.MinBy(c => c.SortOrder); - } - - // We only have a loose leaf or Special left - - var chapters = volumes.First(v => v.IsLooseLeaf() || v.IsSpecial()).Chapters - .OrderBy(c => c.SortOrder) - .ToList(); - - // If there are specials, then return the first Non-special - if (chapters.Exists(c => c.IsSpecial)) - { - var firstChapter = chapters.Find(c => !c.IsSpecial); - if (firstChapter == null) - { - // If there is no non-special chapter, then return first chapter - return chapters[0]; - } - - return firstChapter; - } - // Else use normal logic - return chapters[0]; - } + if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) + { + // I think i need a way to sort volumes last + return volumes.OrderBy(v => v.Number.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters + .OrderBy(c => c.Number.AsFloat()).First(); + } // Loop through all chapters that are not in volume 0 var volumeChapters = volumes - .WhereNotLooseLeaf() + .Where(v => v.Number != 0) .SelectMany(v => v.Chapters) .ToList(); // NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails // If there are any volumes that have progress, return those. If not, move on. var currentlyReadingChapter = volumeChapters - .OrderBy(c => c.MinNumber, _chapterSortComparerDefaultLast) + .OrderBy(c => c.Number.AsDouble(), _chapterSortComparer) .FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0); if (currentlyReadingChapter != null) return currentlyReadingChapter; // Order with volume 0 last so we prefer the natural order - return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, _chapterSortComparerDefaultLast) - .SelectMany(v => v.Chapters.OrderBy(c => c.SortOrder)) + return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default) + .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble())) .ToList()); } @@ -642,7 +591,7 @@ public class ReaderService : IReaderService } - private static int GetNextChapterId(IEnumerable chapters, float currentChapterNumber, Func accessor) + private static int GetNextChapterId(IEnumerable chapters, string currentChapterNumber, Func accessor) { var next = false; var chaptersList = chapters.ToList(); @@ -669,11 +618,11 @@ public class ReaderService : IReaderService public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.OrderBy(v => v.MinNumber)) + foreach (var volume in volumes.OrderBy(v => v.Number)) { var chapters = volume.Chapters - .Where(c => !c.IsSpecial && c.MaxNumber <= chapterNumber) - .OrderBy(c => c.MinNumber); + .Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber) + .OrderBy(c => c.Number.AsFloat()); await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList()); } } @@ -681,7 +630,7 @@ public class ReaderService : IReaderService public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.Where(v => v.MinNumber <= volumeNumber && v.MinNumber > 0).OrderBy(v => v.MinNumber)) + foreach (var volume in volumes.Where(v => v.Number <= volumeNumber && v.Number > 0).OrderBy(v => v.Number)) { await MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } @@ -693,23 +642,21 @@ public class ReaderService : IReaderService { var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 0); var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 0); - return new HourEstimateRangeDto { MinHours = Math.Min(minHours, maxHours), MaxHours = Math.Max(minHours, maxHours), - AvgHours = wordCount / AvgWordsPerHour + AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour)) }; } var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 0); var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 0); - return new HourEstimateRangeDto { MinHours = Math.Min(minHoursPages, maxHoursPages), MaxHours = Math.Max(minHoursPages, maxHoursPages), - AvgHours = pageCount / AvgPagesPerMinute / 60F + AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F)) }; } @@ -783,7 +730,7 @@ public class ReaderService : IReaderService } var files = _directoryService.GetFilesWithExtension(outputDirectory, - Parser.ImageFileExtensions); + Tasks.Scanner.Parser.Parser.ImageFileExtensions); return CacheService.GetPageFromFiles(files, pageNum); } catch (Exception ex) @@ -805,17 +752,14 @@ public class ReaderService : IReaderService { switch(libraryType) { - case LibraryType.Image: case LibraryType.Manga: return "Chapter" + (includeSpace ? " " : string.Empty); case LibraryType.Comic: - case LibraryType.ComicVine: if (includeHash) { return "Issue #"; } return "Issue" + (includeSpace ? " " : string.Empty); case LibraryType.Book: - case LibraryType.LightNovel: return "Book" + (includeSpace ? " " : string.Empty); default: throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null); diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index 6ff8d19de..86deed393 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -2,17 +2,17 @@ using API.Data.Metadata; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; namespace API.Services; #nullable enable public interface IReadingItemService { + ComicInfo? GetComicInfo(string filePath); int GetNumberOfPages(string filePath, MangaFormat format); string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); + ParserInfo? ParseFile(string path, string rootPath, LibraryType type); } public class ReadingItemService : IReadingItemService @@ -21,28 +21,16 @@ public class ReadingItemService : IReadingItemService private readonly IBookService _bookService; private readonly IImageService _imageService; private readonly IDirectoryService _directoryService; - private readonly ILogger _logger; - private readonly BasicParser _basicParser; - private readonly ComicVineParser _comicVineParser; - private readonly ImageParser _imageParser; - private readonly BookParser _bookParser; - private readonly PdfParser _pdfParser; + private readonly IDefaultParser _defaultParser; - public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService, - IDirectoryService directoryService, ILogger logger) + public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService, IDirectoryService directoryService) { _archiveService = archiveService; _bookService = bookService; _imageService = imageService; _directoryService = directoryService; - _logger = logger; - - _imageParser = new ImageParser(directoryService); - _basicParser = new BasicParser(directoryService, _imageParser); - _bookParser = new BookParser(directoryService, bookService, _basicParser); - _comicVineParser = new ComicVineParser(directoryService); - _pdfParser = new PdfParser(directoryService); + _defaultParser = new DefaultParser(directoryService); } /// @@ -50,9 +38,9 @@ public class ReadingItemService : IReadingItemService /// /// Fully qualified path of file /// - private ComicInfo? GetComicInfo(string filePath) + public ComicInfo? GetComicInfo(string filePath) { - if (Parser.IsEpub(filePath) || Parser.IsPdf(filePath)) + if (Parser.IsEpub(filePath)) { return _bookService.GetComicInfo(filePath); } @@ -71,25 +59,78 @@ public class ReadingItemService : IReadingItemService /// Path of a file /// /// Library type to determine parsing to perform - /// Enable Metadata parsing overriding filename parsing - public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo? ParseFile(string path, string rootPath, LibraryType type) { - try + var info = Parse(path, rootPath, type); + if (info == null) { - var info = Parse(path, rootPath, libraryRoot, type, enableMetadata); - if (info == null) - { - _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path); - return null; - } - - return info; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when parsing file {FilePath}", path); return null; } + + + // This catches when original library type is Manga/Comic and when parsing with non + if (Parser.IsEpub(path) && Parser.ParseVolume(info.Series) != Parser.DefaultVolume) // Shouldn't this be info.Volume != DefaultVolume? + { + var hasVolumeInTitle = !Parser.ParseVolume(info.Title) + .Equals(Parser.DefaultVolume); + var hasVolumeInSeries = !Parser.ParseVolume(info.Series) + .Equals(Parser.DefaultVolume); + + if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) + { + // This is likely a light novel for which we can set series from parsed title + info.Series = Parser.ParseSeries(info.Title); + info.Volumes = Parser.ParseVolume(info.Title); + } + else + { + var info2 = _defaultParser.Parse(path, rootPath, LibraryType.Book); + info.Merge(info2); + } + + } + + // This is first time ComicInfo is called + info.ComicInfo = GetComicInfo(path); + if (info.ComicInfo == null) return info; + + if (!string.IsNullOrEmpty(info.ComicInfo.Volume)) + { + info.Volumes = info.ComicInfo.Volume; + } + if (!string.IsNullOrEmpty(info.ComicInfo.Series)) + { + info.Series = info.ComicInfo.Series.Trim(); + } + if (!string.IsNullOrEmpty(info.ComicInfo.Number)) + { + info.Chapters = info.ComicInfo.Number; + } + + // Patch is SeriesSort from ComicInfo + if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort)) + { + info.SeriesSort = info.ComicInfo.TitleSort.Trim(); + } + + if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) + { + info.IsSpecial = true; + info.Chapters = Parser.DefaultChapter; + info.Volumes = Parser.DefaultVolume; + } + + if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort)) + { + info.SeriesSort = info.ComicInfo.SeriesSort.Trim(); + } + + if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries)) + { + info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); + } + + return info; } /// @@ -100,7 +141,6 @@ public class ReadingItemService : IReadingItemService /// public int GetNumberOfPages(string filePath, MangaFormat format) { - switch (format) { case MangaFormat.Archive: @@ -175,31 +215,9 @@ public class ReadingItemService : IReadingItemService /// /// /// - /// /// - private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + private ParserInfo? Parse(string path, string rootPath, LibraryType type) { - if (_comicVineParser.IsApplicable(path, type)) - { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_imageParser.IsApplicable(path, type)) - { - return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_bookParser.IsApplicable(path, type)) - { - return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_pdfParser.IsApplicable(path, type)) - { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - if (_basicParser.IsApplicable(path, type)) - { - return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); - } - - return null; + return Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type); } } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index 8c4f63430..66dc01431 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -22,7 +21,6 @@ using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable public interface IReadingListService { @@ -37,8 +35,8 @@ public interface IReadingListService Task AddChaptersToReadingList(int seriesId, IList chapterIds, ReadingList readingList); - Task ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); - Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); + Task ValidateCblFile(int userId, CblReadingList cblReading); + Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false); Task CalculateStartAndEndDates(ReadingList readingListWithItems); /// /// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user. @@ -47,17 +45,6 @@ public interface IReadingListService /// /// Task CreateReadingListsFromSeries(Series series, Library library); - - Task CreateReadingListsFromSeries(int libraryId, int seriesId); - Task GenerateReadingListCoverImage(int readingListId); - /// - /// Check, and update if needed, all reading lists' AgeRating who contain the passed series - /// - /// The series whose age rating is being updated - /// The new (uncommited) age rating of the series - /// - /// This method does not commit changes - Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating); } /// @@ -69,26 +56,21 @@ public class ReadingListService : IReadingListService private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IEventHub _eventHub; - private readonly IImageService _imageService; - private readonly IDirectoryService _directoryService; - + private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default; private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase, Parser.RegexTimeout); - public ReadingListService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, IImageService imageService, IDirectoryService directoryService) + public ReadingListService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub) { _unitOfWork = unitOfWork; _logger = logger; _eventHub = eventHub; - _imageService = imageService; - _directoryService = directoryService; } public static string FormatTitle(ReadingListItemDto item) { var title = string.Empty; - if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.LooseLeafVolume) { + if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.DefaultVolume) { title = $"Volume {item.VolumeNumber}"; } @@ -104,13 +86,7 @@ public class ReadingListService : IReadingListService { title = $"Volume {Parser.CleanSpecialTitle(item.VolumeNumber)}"; } - } - else if (item.VolumeNumber == Parser.SpecialVolume) - { - title = specialTitle; - } - else - { + } else { title = $"Volume {specialTitle}"; } } @@ -122,30 +98,15 @@ public class ReadingListService : IReadingListService if (title != string.Empty) return title; - // item.ChapterNumber is Range if (item.ChapterNumber == Parser.DefaultChapter && !string.IsNullOrEmpty(item.ChapterTitleName)) { title = item.ChapterTitleName; } - else if (item.IsSpecial && - (!string.IsNullOrEmpty(item.ChapterTitleName) || !string.IsNullOrEmpty(chapterNum))) - { - if (!string.IsNullOrEmpty(item.ChapterTitleName)) - { - title = item.ChapterTitleName; - } - else - { - title = chapterNum; - } - - } else { title = ReaderService.FormatChapterName(item.LibraryType, true, true) + chapterNum; } - return title; } @@ -429,8 +390,8 @@ public class ReadingListService : IReadingListService var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) - .OrderBy(c => c.Volume.MinNumber) - .ThenBy(x => x.SortOrder) + .OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name)) + .ThenBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting) .ToList(); var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1; @@ -445,21 +406,6 @@ public class ReadingListService : IReadingListService return index > lastOrder + 1; } - /// - /// Create Reading lists from a Series - /// - /// Execute this from Hangfire - /// - /// - public async Task CreateReadingListsFromSeries(int libraryId, int seriesId) - { - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); - if (series == null || library == null) return; - - await CreateReadingListsFromSeries(series, library); - } - public async Task CreateReadingListsFromSeries(Series series, Library library) { if (!library.ManageReadingLists) return; @@ -473,7 +419,6 @@ public class ReadingListService : IReadingListService _logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); var user = await _unitOfWork.UserRepository.GetDefaultAdminUser(); series.Metadata ??= new SeriesMetadataBuilder().Build(); - foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) { var pairs = new List>(); @@ -526,12 +471,8 @@ public class ReadingListService : IReadingListService if (!_unitOfWork.HasChanges()) continue; - - _imageService.UpdateColorScape(readingList); await CalculateReadingListAgeRating(readingList); - await _unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic - await CalculateStartAndEndDates(await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter)); await _unitOfWork.CommitAsync(); @@ -554,13 +495,13 @@ public class ReadingListService : IReadingListService var maxPairs = Math.Max(arcs.Length, arcNumbers.Length); for (var i = 0; i < maxPairs; i++) { - var arcNumber = int.MaxValue.ToString(CultureInfo.InvariantCulture); + var arcNumber = int.MaxValue.ToString(); if (arcNumbers.Length > i) { arcNumber = arcNumbers[i]; } - if (string.IsNullOrEmpty(arcs[i]) || !int.TryParse(arcNumber, CultureInfo.InvariantCulture, out _)) continue; + if (string.IsNullOrEmpty(arcs[i]) || !int.TryParse(arcNumber, out _)) continue; data.Add(new Tuple(arcs[i], arcNumber)); } @@ -572,21 +513,19 @@ public class ReadingListService : IReadingListService /// /// /// - /// When true, will force ComicVine library naming conventions: Series (Year) for Series name matching. - public async Task ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false) + public async Task ValidateCblFile(int userId, CblReadingList cblReading) { var importSummary = new CblImportSummaryDto { CblName = cblReading.Name, Success = CblImportResult.Success, - Results = [], + Results = new List(), SuccessfulInserts = new List() }; - if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl; - // Is there another reading list with the same name on the user's account? - if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) + // Is there another reading list with the same name? + if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name)) { importSummary.Success = CblImportResult.Fail; importSummary.Results.Add(new CblBookResult @@ -596,12 +535,10 @@ public class ReadingListService : IReadingListService }); } - - var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); + var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList(); var userSeries = (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); - - if (userSeries.Count == 0) + if (!userSeries.Any()) { // Report that no series exist in the reading list importSummary.Results.Add(new CblBookResult @@ -630,16 +567,6 @@ public class ReadingListService : IReadingListService return importSummary; } - private static string GetSeriesFormatting(CblBook book, bool useComicLibraryMatching) - { - return useComicLibraryMatching ? $"{book.Series} ({book.Volume})" : book.Series; - } - - private static List GetUniqueSeries(CblReadingList cblReading, bool useComicLibraryMatching) - { - return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList(); - } - /// /// Imports (or pretends to) a cbl into a reading list. Call first! @@ -647,9 +574,8 @@ public class ReadingListService : IReadingListService /// /// /// - /// When true, will force ComicVine library naming conventions: Series (Year) for Series name matching. /// - public async Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false) + public async Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems); _logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName); @@ -661,14 +587,13 @@ public class ReadingListService : IReadingListService SuccessfulInserts = new List() }; - var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); + var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList(); var userSeries = (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); - var allSeries = userSeries.ToDictionary(s => s.NormalizedName); - var allSeriesLocalized = userSeries.ToDictionary(s => s.NormalizedLocalizedName); + var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name)); + var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName)); var readingListNameNormalized = Parser.Normalize(cblReading.Name); - // Get all the user's reading lists var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle); if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList)) @@ -693,7 +618,7 @@ public class ReadingListService : IReadingListService readingList.Items ??= new List(); foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i ))) { - var normalizedSeries = Parser.Normalize(GetSeriesFormatting(book, useComicLibraryMatching)); + var normalizedSeries = Parser.Normalize(book.Series); if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries)) { importSummary.Results.Add(new CblBookResult(book) @@ -705,11 +630,9 @@ public class ReadingListService : IReadingListService } // Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter var bookVolume = string.IsNullOrEmpty(book.Volume) - ? Parser.LooseLeafVolume + ? Parser.DefaultVolume : book.Volume; - var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) - ?? bookSeries.Volumes.GetLooseLeafVolumeOrDefault() - ?? bookSeries.Volumes.GetSpecialVolumeOrDefault(); + var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0); if (matchingVolume == null) { importSummary.Results.Add(new CblBookResult(book) @@ -721,11 +644,11 @@ public class ReadingListService : IReadingListService continue; } - // We need to handle default chapter or empty string when it's just a volume + // We need to handle chapter 0 or empty string when it's just a volume var bookNumber = string.IsNullOrEmpty(book.Number) ? Parser.DefaultChapter : book.Number; - var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Range == bookNumber); + var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber); if (chapter == null) { importSummary.Results.Add(new CblBookResult(book) @@ -773,10 +696,7 @@ public class ReadingListService : IReadingListService } // If there are no items, don't create a blank list - if (!_unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary; - - - _imageService.UpdateColorScape(readingList); + if (!_unitOfWork.HasChanges() || !readingList.Items.Any()) return importSummary; await _unitOfWork.CommitAsync(); @@ -786,7 +706,7 @@ public class ReadingListService : IReadingListService private static IList FindCblImportConflicts(IEnumerable userSeries) { var dict = new HashSet(); - return userSeries.Where(series => !dict.Add(series.NormalizedName)).ToList(); + return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList(); } private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary, @@ -827,51 +747,4 @@ public class ReadingListService : IReadingListService file.Close(); return cblReadingList; } - - public async Task GenerateReadingListCoverImage(int readingListId) - { - // TODO: Currently reading lists are dynamically generated at runtime. This needs to be overhauled to be generated and stored within - // the Reading List (and just expire every so often) so we can utilize ColorScapes. - // Check if a cover already exists for the reading list - // var potentialExistingCoverPath = _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, - // ImageService.GetReadingListFormat(readingListId)); - // if (_directoryService.FileSystem.File.Exists(potentialExistingCoverPath)) - // { - // // Check if we need to update CoverScape - // - // } - - var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); - var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, - ImageService.GetReadingListFormat(readingListId)); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - destFile += settings.EncodeMediaAs.GetExtension(); - - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; - ImageService.CreateMergedImage( - covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), - settings.CoverImageSize, - destFile); - // TODO: Refactor this so that reading lists have a dedicated cover image so we can calculate primary/secondary colors - - return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; - } - - public async Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating) - { - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsBySeriesId(seriesId); - foreach (var readingList in readingLists) - { - var seriesIds = readingList.Items.Select(item => item.SeriesId).ToList(); - seriesIds.Remove(seriesId); // Don't get AgeRating from database - - var maxAgeRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); - if (ageRating > maxAgeRating) - { - maxAgeRating = ageRating; - } - - readingList.AgeRating = maxAgeRating; - } - } } diff --git a/API/Services/ReadingProfileService.cs b/API/Services/ReadingProfileService.cs deleted file mode 100644 index 4c3dab006..000000000 --- a/API/Services/ReadingProfileService.cs +++ /dev/null @@ -1,454 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using AutoMapper; -using Kavita.Common; - -namespace API.Services; -#nullable enable - -public interface IReadingProfileService -{ - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series (Implicit) -> Series (User) -> Library (User) -> Default - /// - /// - /// - /// - /// - Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false); - - /// - /// Creates a new reading profile for a user. Name must be unique per user - /// - /// - /// - /// - Task CreateReadingProfile(int userId, UserReadingProfileDto dto); - Task PromoteImplicitProfile(int userId, int profileId); - - /// - /// Updates the implicit reading profile for a series, creates one if none exists - /// - /// - /// - /// - /// - Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto); - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - /// - Task UpdateParent(int userId, int seriesId, UserReadingProfileDto dto); - - /// - /// Updates a given reading profile for a user - /// - /// - /// - /// - /// Does not update connected series and libraries - Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); - - /// - /// Deletes a given profile for a user - /// - /// - /// - /// - /// - /// The default profile for the user cannot be deleted - Task DeleteReadingProfile(int userId, int profileId); - - /// - /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task AddProfileToSeries(int userId, int profileId, int seriesId); - /// - /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds); - /// - /// Remove all reading profiles bound to the series - /// - /// - /// - /// - Task ClearSeriesProfile(int userId, int seriesId); - - /// - /// Bind the reading profile to the library - /// - /// - /// - /// - /// - Task AddProfileToLibrary(int userId, int profileId, int libraryId); - /// - /// Remove the reading profile bound to the library, if it exists - /// - /// - /// - /// - Task ClearLibraryProfile(int userId, int libraryId); - /// - /// Returns the bound Reading Profile to a Library - /// - /// - /// - /// - Task GetReadingProfileDtoForLibrary(int userId, int libraryId); -} - -public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService -{ - /// - /// Tries to resolve the Reading Profile for a given Series. Will first check (optionally) Implicit profiles, then check for a bound Series profile, then a bound - /// Library profile, then default to the default profile. - /// - /// - /// - /// - /// - /// - public async Task GetReadingProfileForSeries(int userId, int seriesId, bool skipImplicit = false) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, skipImplicit); - - // If there is an implicit, send back - var implicitProfile = - profiles.FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind == ReadingProfileKind.Implicit); - if (implicitProfile != null) return implicitProfile; - - // Next check for a bound Series profile - var seriesProfile = profiles - .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind != ReadingProfileKind.Implicit); - if (seriesProfile != null) return seriesProfile; - - // Check for a library bound profile - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); - - var libraryProfile = profiles - .FirstOrDefault(p => p.LibraryIds.Contains(series.LibraryId) && p.Kind != ReadingProfileKind.Implicit); - if (libraryProfile != null) return libraryProfile; - - // Fallback to the default profile - return profiles.First(p => p.Kind == ReadingProfileKind.Default); - } - - public async Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false) - { - return mapper.Map(await GetReadingProfileForSeries(userId, seriesId, skipImplicit)); - } - - public async Task UpdateParent(int userId, int seriesId, UserReadingProfileDto dto) - { - var parentProfile = await GetReadingProfileForSeries(userId, seriesId, true); - - UpdateReaderProfileFields(parentProfile, dto, false); - unitOfWork.AppUserReadingProfileRepository.Update(parentProfile); - - // Remove the implicit profile when we UpdateParent (from reader) as it is implied that we are already bound with a non-implicit profile - await DeleteImplicateReadingProfilesForSeries(userId, [seriesId]); - - await unitOfWork.CommitAsync(); - return mapper.Map(parentProfile); - } - - public async Task UpdateReadingProfile(int userId, UserReadingProfileDto dto) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, dto.Id); - if (profile == null) throw new KavitaException("profile-does-not-exist"); - - UpdateReaderProfileFields(profile, dto); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - return mapper.Map(profile); - } - - public async Task CreateReadingProfile(int userId, UserReadingProfileDto dto) - { - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - if (await unitOfWork.AppUserReadingProfileRepository.IsProfileNameInUse(userId, dto.Name)) throw new KavitaException("name-already-in-use"); - - var newProfile = new AppUserReadingProfileBuilder(user.Id).Build(); - UpdateReaderProfileFields(newProfile, dto); - - unitOfWork.AppUserReadingProfileRepository.Add(newProfile); - user.ReadingProfiles.Add(newProfile); - - await unitOfWork.CommitAsync(); - - return mapper.Map(newProfile); - } - - /// - /// Promotes the implicit profile to a user profile. Removes the series from other profiles. - /// - /// - /// - /// - public async Task PromoteImplicitProfile(int userId, int profileId) - { - // Get all the user's profiles including the implicit - var allUserProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, false); - var profileToPromote = allUserProfiles.First(r => r.Id == profileId); - var seriesId = profileToPromote.SeriesIds[0]; // An Implicit series can only be bound to 1 Series - - // Check if there are any reading profiles (Series) already bound to the series - var existingSeriesProfile = allUserProfiles.FirstOrDefault(r => r.SeriesIds.Contains(seriesId) && r.Kind == ReadingProfileKind.User); - if (existingSeriesProfile != null) - { - existingSeriesProfile.SeriesIds.Remove(seriesId); - unitOfWork.AppUserReadingProfileRepository.Update(existingSeriesProfile); - } - - // Convert the implicit profile into a proper Series - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - if (series == null) throw new KavitaException("series-doesnt-exist"); // Shouldn't happen - - profileToPromote.Kind = ReadingProfileKind.User; - profileToPromote.Name = await localizationService.Translate(userId, "generated-reading-profile-name", series.Name); - profileToPromote.Name = EnsureUniqueProfileName(allUserProfiles, profileToPromote.Name); - profileToPromote.NormalizedName = profileToPromote.Name.ToNormalized(); - unitOfWork.AppUserReadingProfileRepository.Update(profileToPromote); - - await unitOfWork.CommitAsync(); - - return mapper.Map(profileToPromote); - } - - private static string EnsureUniqueProfileName(IList allUserProfiles, string name) - { - var counter = 1; - var newName = name; - while (allUserProfiles.Any(p => p.Name == newName)) - { - newName = $"{name} ({counter})"; - counter++; - } - - return newName; - } - - public async Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto) - { - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var existingProfile = profiles.FirstOrDefault(rp => rp.Kind == ReadingProfileKind.Implicit && rp.SeriesIds.Contains(seriesId)); - - // Series already had an implicit profile, update it - if (existingProfile is {Kind: ReadingProfileKind.Implicit}) - { - UpdateReaderProfileFields(existingProfile, dto, false); - unitOfWork.AppUserReadingProfileRepository.Update(existingProfile); - await unitOfWork.CommitAsync(); - - return mapper.Map(existingProfile); - } - - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId) ?? throw new KeyNotFoundException(); - var newProfile = new AppUserReadingProfileBuilder(userId) - .WithSeries(series) - .WithKind(ReadingProfileKind.Implicit) - .Build(); - - // Set name to something fitting for debugging if needed - UpdateReaderProfileFields(newProfile, dto, false); - newProfile.Name = $"Implicit Profile for {seriesId}"; - newProfile.NormalizedName = newProfile.Name.ToNormalized(); - - user.ReadingProfiles.Add(newProfile); - await unitOfWork.CommitAsync(); - - return mapper.Map(newProfile); - } - - public async Task DeleteReadingProfile(int userId, int profileId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - if (profile.Kind == ReadingProfileKind.Default) throw new KavitaException("cant-delete-default-profile"); - - unitOfWork.AppUserReadingProfileRepository.Remove(profile); - await unitOfWork.CommitAsync(); - } - - public async Task AddProfileToSeries(int userId, int profileId, int seriesId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); - - profile.SeriesIds.Add(seriesId); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - } - - public async Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, seriesIds, []); - - profile.SeriesIds.AddRange(seriesIds.Except(profile.SeriesIds)); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - } - - public async Task ClearSeriesProfile(int userId, int seriesId) - { - await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); - await unitOfWork.CommitAsync(); - } - - public async Task AddProfileToLibrary(int userId, int profileId, int libraryId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, [], [libraryId]); - - profile.LibraryIds.Add(libraryId); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - await unitOfWork.CommitAsync(); - } - - public async Task ClearLibraryProfile(int userId, int libraryId) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var libraryProfile = profiles.FirstOrDefault(p => p.LibraryIds.Contains(libraryId)); - if (libraryProfile != null) - { - libraryProfile.LibraryIds.Remove(libraryId); - unitOfWork.AppUserReadingProfileRepository.Update(libraryProfile); - } - - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - } - - public async Task GetReadingProfileDtoForLibrary(int userId, int libraryId) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, true); - return mapper.Map(profiles.FirstOrDefault(p => p.LibraryIds.Contains(libraryId))); - } - - private async Task DeleteImplicitAndRemoveFromUserProfiles(int userId, IList seriesIds, IList libraryIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var implicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.Implicit) - .ToList(); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); - - var nonImplicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any() || rp.LibraryIds.Intersect(libraryIds).Any()) - .Where(rp => rp.Kind != ReadingProfileKind.Implicit); - - foreach (var profile in nonImplicitProfiles) - { - profile.SeriesIds.RemoveAll(seriesIds.Contains); - profile.LibraryIds.RemoveAll(libraryIds.Contains); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - } - } - - private async Task DeleteImplicateReadingProfilesForSeries(int userId, IList seriesIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var implicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.Implicit) - .ToList(); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); - } - - private async Task RemoveSeriesFromUserProfiles(int userId, IList seriesIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var userProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.User) - .ToList(); - - unitOfWork.AppUserReadingProfileRepository.RemoveRange(userProfiles); - } - - public static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto, bool updateName = true) - { - if (updateName && !string.IsNullOrEmpty(dto.Name) && existingProfile.NormalizedName != dto.Name.ToNormalized()) - { - existingProfile.Name = dto.Name; - existingProfile.NormalizedName = dto.Name.ToNormalized(); - } - - // Manga Reader - existingProfile.ReadingDirection = dto.ReadingDirection; - existingProfile.ScalingOption = dto.ScalingOption; - existingProfile.PageSplitOption = dto.PageSplitOption; - existingProfile.ReaderMode = dto.ReaderMode; - existingProfile.AutoCloseMenu = dto.AutoCloseMenu; - existingProfile.ShowScreenHints = dto.ShowScreenHints; - existingProfile.EmulateBook = dto.EmulateBook; - existingProfile.LayoutMode = dto.LayoutMode; - existingProfile.BackgroundColor = string.IsNullOrEmpty(dto.BackgroundColor) ? "#000000" : dto.BackgroundColor; - existingProfile.SwipeToPaginate = dto.SwipeToPaginate; - existingProfile.AllowAutomaticWebtoonReaderDetection = dto.AllowAutomaticWebtoonReaderDetection; - existingProfile.WidthOverride = dto.WidthOverride; - existingProfile.DisableWidthOverride = dto.DisableWidthOverride; - - // Book Reader - existingProfile.BookReaderMargin = dto.BookReaderMargin; - existingProfile.BookReaderLineSpacing = dto.BookReaderLineSpacing; - existingProfile.BookReaderFontSize = dto.BookReaderFontSize; - existingProfile.BookReaderFontFamily = dto.BookReaderFontFamily; - existingProfile.BookReaderTapToPaginate = dto.BookReaderTapToPaginate; - existingProfile.BookReaderReadingDirection = dto.BookReaderReadingDirection; - existingProfile.BookReaderWritingStyle = dto.BookReaderWritingStyle; - existingProfile.BookThemeName = dto.BookReaderThemeName; - existingProfile.BookReaderLayoutMode = dto.BookReaderLayoutMode; - existingProfile.BookReaderImmersiveMode = dto.BookReaderImmersiveMode; - - // PDF Reading - existingProfile.PdfTheme = dto.PdfTheme; - existingProfile.PdfScrollMode = dto.PdfScrollMode; - existingProfile.PdfSpreadMode = dto.PdfSpreadMode; - } -} diff --git a/API/Services/ReviewService.cs b/API/Services/ReviewService.cs new file mode 100644 index 000000000..6ad170df8 --- /dev/null +++ b/API/Services/ReviewService.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs.SeriesDetail; +using API.Entities; +using API.Entities.Enums; +using API.Helpers; +using API.Helpers.Builders; +using API.Services.Plus; +using Flurl.Http; +using HtmlAgilityPack; +using Kavita.Common; +using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Helpers; +using Microsoft.Extensions.Logging; + +namespace API.Services; + +internal class MediaReviewDto +{ + public string Body { get; set; } + public string Tagline { get; set; } + public int Rating { get; set; } + public int TotalVotes { get; set; } + /// + /// The media's overall Score + /// + public int Score { get; set; } + public string SiteUrl { get; set; } + /// + /// In Markdown + /// + public string RawBody { get; set; } + public string Username { get; set; } + public ScrobbleProvider Provider { get; set; } +} + +public interface IReviewService +{ + Task> GetReviewsForSeries(int userId, int seriesId); +} + +public class ReviewService : IReviewService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + + public ReviewService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + } + + public async Task> GetReviewsForSeries(int userId, int seriesId) + { + var series = + await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null || series == null) return new List(); + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var ret = (await GetReviews(license.Value, series)).Select(r => new UserReviewDto() + { + Body = r.Body, + Tagline = r.Tagline, + Score = r.Score, + Username = r.Username, + LibraryId = series.LibraryId, + SeriesId = series.Id, + IsExternal = true, + Provider = r.Provider, + BodyJustText = GetCharacters(r.Body), + ExternalUrl = r.SiteUrl + }); + + return ret.OrderByDescending(r => r.Score); + } + + private static string GetCharacters(string body) + { + if (string.IsNullOrEmpty(body)) return body; + + var doc = new HtmlDocument(); + doc.LoadHtml(body); + + var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]"); + if (textNodes == null) return string.Empty; + var plainText = string.Join(" ", textNodes + .Select(node => node.InnerText) + .Where(s => !s.Equals("\n"))); + + // Clean any leftover markdown out + plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty); + plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty); + plainText = Regex.Replace(plainText, @"~~~(.*?)~~~", "$1"); + plainText = Regex.Replace(plainText, @"\+{3}(.*?)\+{3}", "$1"); + plainText = Regex.Replace(plainText, @"~~(.*?)~~", "$1"); + plainText = Regex.Replace(plainText, @"__(.*?)__", "$1"); + plainText = Regex.Replace(plainText, @"#\s(.*?)", "$1"); + + // Just strip symbols + plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty); + plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty); + plainText = Regex.Replace(plainText, @"~~~", string.Empty); + plainText = Regex.Replace(plainText, @"\+", string.Empty); + plainText = Regex.Replace(plainText, @"~~", string.Empty); + plainText = Regex.Replace(plainText, @"__", string.Empty); + + // Take the first 100 characters + plainText = plainText.Length > 100 ? plainText.Substring(0, 100) : plainText; + + return plainText + "…"; + } + + + private async Task> GetReviews(string license, Series series) + { + _logger.LogDebug("Fetching external reviews for Series: {SeriesName}", series.Name); + try + { + return await (Configuration.KavitaPlusApiUrl + "/api/review") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) + .ReceiveJson>(); + + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to Kavita+ API"); + } + + return new List(); + } +} diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 78e3c41f1..4bc3aec09 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -7,41 +8,38 @@ using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.Person; +using API.DTOs.CollectionTags; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.Interfaces; using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable + public interface ISeriesService { Task GetSeriesDetail(int seriesId, int userId); Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto); + Task UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto); Task DeleteMultipleSeries(IList seriesIds); Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); Task GetRelatedSeries(int userId, int seriesId); Task FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true); Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true); - Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, + + Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash); Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false); Task GetEstimatedChapterCreationDate(int seriesId, int userId); - } public class SeriesService : ISeriesService @@ -52,18 +50,16 @@ public class SeriesService : ISeriesService private readonly ILogger _logger; private readonly IScrobblingService _scrobblingService; private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto + private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto() { ExpectedDate = null, ChapterNumber = 0, - VolumeNumber = Parser.LooseLeafVolumeNumber + VolumeNumber = 0 }; public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler, - ILogger logger, IScrobblingService scrobblingService, ILocalizationService localizationService, - IReadingListService readingListService) + ILogger logger, IScrobblingService scrobblingService, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _eventHub = eventHub; @@ -71,78 +67,86 @@ public class SeriesService : ISeriesService _logger = logger; _scrobblingService = scrobblingService; _localizationService = localizationService; - _readingListService = readingListService; + } /// - /// Returns the first chapter for a series to extract metadata from (ie Summary, etc.) + /// Returns the first chapter for a series to extract metadata from (ie Summary, etc) /// /// The full series with all volumes and chapters on it /// public static Chapter? GetFirstChapterForMetadata(Series series) { var sortedVolumes = series.Volumes - .Where(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber)) - .OrderBy(v => v.MinNumber); - var minVolumeNumber = sortedVolumes.MinBy(v => v.MinNumber); + .Where(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) && parsedValue != 0.0f) + .OrderBy(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : float.MaxValue); + var minVolumeNumber = sortedVolumes + .MinBy(v => v.Name.AsFloat()); var allChapters = series.Volumes - .SelectMany(v => v.Chapters.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default)) + .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)) .ToList(); var minChapter = allChapters .FirstOrDefault(); - if (minVolumeNumber != null && minChapter != null && - (minChapter.MinNumber >= minVolumeNumber.MinNumber || minChapter.MinNumber.Is(Parser.DefaultChapterNumber))) + if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && + (chapNum >= minVolumeNumber.Number || chapNum == 0)) { - return minVolumeNumber.Chapters.MinBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default); + return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } return minChapter; } - /// - /// Updates the Series Metadata. - /// - /// - /// public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) { try { var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) return false; + var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList(); + var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList(); + var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList(); + var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList(); series.Metadata ??= new SeriesMetadataBuilder() + .WithCollectionTags(updateSeriesMetadataDto.CollectionTags.Select(dto => + new CollectionTagBuilder(dto.Title) + .WithId(dto.Id) + .WithSummary(dto.Summary) + .WithIsPromoted(dto.Promoted) + .Build()).ToList()) .Build(); + if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating) + { + series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata.AgeRating; + series.Metadata.AgeRatingLocked = true; + } + if (NumberHelper.IsValidYear(updateSeriesMetadataDto.SeriesMetadata.ReleaseYear) && series.Metadata.ReleaseYear != updateSeriesMetadataDto.SeriesMetadata.ReleaseYear) { series.Metadata.ReleaseYear = updateSeriesMetadataDto.SeriesMetadata.ReleaseYear; series.Metadata.ReleaseYearLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.StartDate); } if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus) { series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus; series.Metadata.PublicationStatusLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.PublicationStatus); } if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) { updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary); } if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim()) { series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty; series.Metadata.SummaryLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary); } if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language) @@ -156,181 +160,86 @@ public class SeriesService : ISeriesService series.Metadata.WebLinks = string.Empty; } else { - series.Metadata.WebLinks = string.Join(',', updateSeriesMetadataDto.SeriesMetadata?.WebLinks - .Split(',') + series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks + .Split(",") .Where(s => !string.IsNullOrEmpty(s)) .Select(s => s.Trim())! ); } - if (updateSeriesMetadataDto.SeriesMetadata?.Genres != null && - updateSeriesMetadataDto.SeriesMetadata.Genres.Count != 0) + series.Metadata.CollectionTags ??= new List(); + UpdateCollectionsList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) => { - var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)))).ToList(); - series.Metadata.Genres ??= []; - GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, genre => - { - series.Metadata.Genres.Add(genre); - }, () => series.Metadata.GenresLocked = true); - } - else + series.Metadata.CollectionTags.Add(tag); + }); + + series.Metadata.Genres ??= new List(); + GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => { - series.Metadata.Genres = []; + series.Metadata.Genres.Add(genre); + }, () => series.Metadata.GenresLocked = true); + + series.Metadata.Tags ??= new List(); + TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, (tag) => + { + series.Metadata.Tags.Add(tag); + }, () => series.Metadata.TagsLocked = true); + + void HandleAddPerson(Person person) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + allPeople.Add(person); } + series.Metadata.People ??= new List(); + PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allPeople, + HandleAddPerson, () => series.Metadata.WriterLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople, + HandleAddPerson, () => series.Metadata.CharacterLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople, + HandleAddPerson, () => series.Metadata.ColoristLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople, + HandleAddPerson, () => series.Metadata.EditorLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople, + HandleAddPerson, () => series.Metadata.InkerLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople, + HandleAddPerson, () => series.Metadata.LettererLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople, + HandleAddPerson, () => series.Metadata.PencillerLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople, + HandleAddPerson, () => series.Metadata.PublisherLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople, + HandleAddPerson, () => series.Metadata.TranslatorLocked = true); + PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople, + HandleAddPerson, () => series.Metadata.CoverArtistLocked = true); - if (updateSeriesMetadataDto.SeriesMetadata?.Tags is {Count: > 0}) - { - var allTags = (await _unitOfWork.TagRepository - .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)))) - .ToList(); - series.Metadata.Tags ??= []; - TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, tag => - { - series.Metadata.Tags.Add(tag); - }, () => series.Metadata.TagsLocked = true); - } - else - { - series.Metadata.Tags = []; - } - - if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata?.AgeRating) - { - series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown; - series.Metadata.AgeRatingLocked = true; - await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); - } - else - { - if (!series.Metadata.AgeRatingLocked) - { - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - var allTags = series.Metadata.Tags.Select(t => t.Title).Concat(series.Metadata.Genres.Select(g => g.Title)); - var updatedRating = ExternalMetadataService.DetermineAgeRating(allTags, metadataSettings.AgeRatingMappings); - if (updatedRating > series.Metadata.AgeRating) - { - series.Metadata.AgeRating = updatedRating; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); - } - } - } - - // Update people and locks - if (updateSeriesMetadataDto.SeriesMetadata != null) - { - series.Metadata.People ??= []; - - // Writers - if (!series.Metadata.WriterLocked || !updateSeriesMetadataDto.SeriesMetadata.WriterLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Writers, PersonRole.Writer, _unitOfWork); - } - - // Cover Artists - if (!series.Metadata.CoverArtistLocked || !updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, PersonRole.CoverArtist, _unitOfWork); - } - - // Colorists - if (!series.Metadata.ColoristLocked || !updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Colorists, PersonRole.Colorist, _unitOfWork); - } - - // Editors - if (!series.Metadata.EditorLocked || !updateSeriesMetadataDto.SeriesMetadata.EditorLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Editors, PersonRole.Editor, _unitOfWork); - } - - // Inkers - if (!series.Metadata.InkerLocked || !updateSeriesMetadataDto.SeriesMetadata.InkerLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Inkers, PersonRole.Inker, _unitOfWork); - } - - // Letterers - if (!series.Metadata.LettererLocked || !updateSeriesMetadataDto.SeriesMetadata.LettererLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Letterers, PersonRole.Letterer, _unitOfWork); - } - - // Pencillers - if (!series.Metadata.PencillerLocked || !updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Pencillers, PersonRole.Penciller, _unitOfWork); - } - - // Publishers - if (!series.Metadata.PublisherLocked || !updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Publishers, PersonRole.Publisher, _unitOfWork); - } - - // Imprints - if (!series.Metadata.ImprintLocked || !updateSeriesMetadataDto.SeriesMetadata.ImprintLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Imprints, PersonRole.Imprint, _unitOfWork); - } - - // Teams - if (!series.Metadata.TeamLocked || !updateSeriesMetadataDto.SeriesMetadata.TeamLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Teams, PersonRole.Team, _unitOfWork); - } - - // Locations - if (!series.Metadata.LocationLocked || !updateSeriesMetadataDto.SeriesMetadata.LocationLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Locations, PersonRole.Location, _unitOfWork); - } - - // Translators - if (!series.Metadata.TranslatorLocked || !updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Translators, PersonRole.Translator, _unitOfWork); - } - - // Characters - if (!series.Metadata.CharacterLocked || !updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) - { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Characters, PersonRole.Character, _unitOfWork); - } - - series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; - series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked; - series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked; - series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked; - series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked; - series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharacterLocked; - series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristLocked; - series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorLocked; - series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkerLocked; - series.Metadata.ImprintLocked = updateSeriesMetadataDto.SeriesMetadata.ImprintLocked; - series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LettererLocked; - series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillerLocked; - series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked; - series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked; - series.Metadata.LocationLocked = updateSeriesMetadataDto.SeriesMetadata.LocationLocked; - series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked; - series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WriterLocked; - series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked; - series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked; - } + series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; + series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked; + series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked; + series.Metadata.GenresLocked = updateSeriesMetadataDto.SeriesMetadata.GenresLocked; + series.Metadata.TagsLocked = updateSeriesMetadataDto.SeriesMetadata.TagsLocked; + series.Metadata.CharacterLocked = updateSeriesMetadataDto.SeriesMetadata.CharactersLocked; + series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristsLocked; + series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorsLocked; + series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkersLocked; + series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LetterersLocked; + series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillersLocked; + series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublishersLocked; + series.Metadata.TranslatorLocked = updateSeriesMetadataDto.SeriesMetadata.TranslatorsLocked; + series.Metadata.CoverArtistLocked = updateSeriesMetadataDto.SeriesMetadata.CoverArtistsLocked; + series.Metadata.WriterLocked = updateSeriesMetadataDto.SeriesMetadata.WritersLocked; + series.Metadata.SummaryLocked = updateSeriesMetadataDto.SeriesMetadata.SummaryLocked; + series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked; if (!_unitOfWork.HasChanges()) { return true; } - _unitOfWork.SeriesRepository.Update(series.Metadata); await _unitOfWork.CommitAsync(); - // Trigger code to clean up tags, collections, people, etc + // Trigger code to cleanup tags, collections, people, etc try { await _taskScheduler.CleanupDbEntries(); @@ -340,6 +249,14 @@ public class SeriesService : ISeriesService _logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); } + + if (updateSeriesMetadataDto.CollectionTags == null) return true; + foreach (var tag in updateSeriesMetadataDto.CollectionTags) + { + await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection, + MessageFactory.SeriesAddedToCollectionEvent(tag.Id, + updateSeriesMetadataDto.SeriesMetadata.SeriesId), false); + } return true; } catch (Exception ex) @@ -351,115 +268,102 @@ public class SeriesService : ISeriesService return false; } - /// - /// Exclusively for Series Update API - /// - /// - /// - /// - public static async Task HandlePeopleUpdateAsync(SeriesMetadata metadata, ICollection peopleDtos, PersonRole role, IUnitOfWork unitOfWork) + + public static void UpdateCollectionsList(ICollection? tags, Series series, IReadOnlyCollection allTags, + Action handleAdd) { - // TODO: Cleanup this code so we aren't using UnitOfWork like this - - // Normalize all names from the DTOs - var normalizedNames = peopleDtos - .Select(p => Parser.Normalize(p.Name)) - .Distinct() - .ToList(); - - // Bulk select people who already exist in the database - var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames); - - // Use a dictionary for quick lookups - var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); - - // List to track people that will be added to the metadata - var peopleToAdd = new List(); - - foreach (var personDto in peopleDtos) + // TODO: Move UpdateCollectionsList to a helper so we can easily test + if (tags == null) return; + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.CollectionTags.ToList(); + foreach (var existing in existingTags) { - var normalizedPersonName = Parser.Normalize(personDto.Name); - - // Check if the person exists in the dictionary - if (existingPeopleDictionary.TryGetValue(normalizedPersonName, out var p)) + if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) { - // TODO: Should I add more controls here to map back? - if (personDto.AniListId > 0 && p.AniListId <= 0 && p.AniListId != personDto.AniListId) + // Remove tag + series.Metadata.CollectionTags.Remove(existing); + } + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tag in tags) + { + var existingTag = allTags.SingleOrDefault(t => t.Title == tag.Title); + if (existingTag != null) + { + if (series.Metadata.CollectionTags.All(t => t.Title != tag.Title)) { - p.AniListId = personDto.AniListId; + handleAdd(existingTag); } - p.Description = string.IsNullOrEmpty(p.Description) ? personDto.Description : p.Description; - continue; // If we ever want to update metadata for existing people, we'd do it here } - - // Person doesn't exist, so create a new one - var newPerson = new Person + else { - Name = personDto.Name, - NormalizedName = normalizedPersonName, - AniListId = personDto.AniListId, - Description = personDto.Description, - Asin = personDto.Asin, - CoverImage = personDto.CoverImage, - MalId = personDto.MalId, - HardcoverId = personDto.HardcoverId, - }; - - peopleToAdd.Add(newPerson); - existingPeopleDictionary[normalizedPersonName] = newPerson; + // Add new tag + handleAdd(new CollectionTagBuilder(tag.Title) + .WithId(tag.Id) + .WithSummary(tag.Summary) + .WithIsPromoted(tag.Promoted) + .Build()); + } } - - // Add any new people to the database in bulk - if (peopleToAdd.Count != 0) - { - unitOfWork.PersonRepository.Attach(peopleToAdd); - } - - // Now that we have all the people (new and existing), update the SeriesMetadataPeople - UpdateSeriesMetadataPeople(metadata, metadata.People, existingPeopleDictionary.Values, role); } - private static void UpdateSeriesMetadataPeople(SeriesMetadata metadata, ICollection metadataPeople, IEnumerable people, PersonRole role) + /// + /// + /// + /// User with Ratings includes + /// + /// + public async Task UpdateRating(AppUser? user, UpdateSeriesRatingDto updateSeriesRatingDto) { - var peopleToAdd = people.ToList(); - - // Remove any people in the existing metadataPeople for this role that are no longer present in the input list - var peopleToRemove = metadataPeople - .Where(mp => mp.Role == role && peopleToAdd.TrueForAll(p => p.NormalizedName != mp.Person.NormalizedName)) - .ToList(); - - foreach (var personToRemove in peopleToRemove) + if (user == null) { - metadataPeople.Remove(personToRemove); + _logger.LogError("Cannot update rating of null user"); + return false; } - // Add new people for this role if they don't already exist - foreach (var person in peopleToAdd) + var userRating = + await _unitOfWork.UserRepository.GetUserRatingAsync(updateSeriesRatingDto.SeriesId, user.Id) ?? + new AppUserRating(); + try { - var existingPersonEntry = metadataPeople - .FirstOrDefault(mp => mp.Person.NormalizedName == person.NormalizedName && mp.Role == role); + userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateSeriesRatingDto.SeriesId; - if (existingPersonEntry == null) + if (userRating.Id == 0) { - metadataPeople.Add(new SeriesMetadataPeople - { - PersonId = person.Id, - Person = person, - SeriesMetadataId = metadata.Id, - SeriesMetadata = metadata, - Role = role - }); + user.Ratings ??= new List(); + user.Ratings.Add(userRating); + } + + _unitOfWork.UserRepository.Update(user); + + if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + { + BackgroundJob.Enqueue(() => + _scrobblingService.ScrobbleRatingUpdate(user.Id, updateSeriesRatingDto.SeriesId, + userRating.Rating)); + return true; } } - } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception saving rating"); + } + await _unitOfWork.RollbackAsync(); + user.Ratings?.Remove(userRating); + + return false; + } public async Task DeleteMultipleSeries(IList seriesIds) { try { var chapterMappings = - await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds]); + await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(seriesIds.ToArray()); var allChapterIds = new List(); foreach (var mapping in chapterMappings) @@ -467,10 +371,7 @@ public class SeriesService : ISeriesService allChapterIds.AddRange(mapping.Value); } - // NOTE: This isn't getting all the people and whatnot currently due to the lack of includes var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds); - _unitOfWork.SeriesRepository.Remove(series); - var libraryIds = series.Select(s => s.LibraryId); var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds); foreach (var library in libraries) @@ -478,8 +379,11 @@ public class SeriesService : ISeriesService library.UpdateLastModified(); _unitOfWork.LibraryRepository.Update(library); } - await _unitOfWork.CommitAsync(); + _unitOfWork.SeriesRepository.Remove(series); + + + if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) return true; foreach (var s in series) { @@ -488,16 +392,16 @@ public class SeriesService : ISeriesService } await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - _taskScheduler.CleanupChapters([.. allChapterIds]); - - return true; + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + _taskScheduler.CleanupChapters(allChapterIds.ToArray()); } catch (Exception ex) { _logger.LogError(ex, "There was an issue when trying to delete multiple series"); return false; } + + return true; } /// @@ -524,61 +428,77 @@ public class SeriesService : ISeriesService var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel; - var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId); + var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) + .OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name)) + .ToList(); // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. var processedVolumes = new List(); - foreach (var volume in volumes) + if (libraryType == LibraryType.Book) { - if (volume.IsLooseLeaf() || volume.IsSpecial()) - { - continue; - } - - if (RenameVolumeName(volume, libraryType, volumeLabel) || (bookTreatment && !volume.IsSpecial())) + var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); + foreach (var volume in volumes) { + volume.Chapters = volume.Chapters + .OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default) + .ToList(); + var firstChapter = volume.Chapters.First(); + // On Books, skip volumes that are specials, since these will be shown + if (firstChapter.IsSpecial) continue; + RenameVolumeName(firstChapter, volume, libraryType, volumeLabel); processedVolumes.Add(volume); } } + else + { + processedVolumes = volumes.Where(v => v.Number > 0).ToList(); + processedVolumes.ForEach(v => + { + v.Name = $"Volume {v.Name}"; + v.Chapters = v.Chapters.OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default).ToList(); + }); + } var specials = new List(); - // Why isn't this doing a check if chapter is not special as it wont get included - var chapters = volumes - .SelectMany(v => v.Chapters - .Select(c => - { - if (v.IsLooseLeaf() || v.IsSpecial()) return c; - c.VolumeTitle = v.Name; - return c; - }) - .OrderBy(c => c.SortOrder)) - .ToList(); + var chapters = volumes.SelectMany(v => v.Chapters.Select(c => + { + if (v.Number == 0) return c; + c.VolumeTitle = v.Name; + return c; + }).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); foreach (var chapter in chapters) { chapter.Title = await FormatChapterTitle(userId, chapter, libraryType); - if (!chapter.IsSpecial) continue; + + if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName; specials.Add(chapter); } - // Don't show chapter -100000 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes) - IEnumerable retChapters = bookTreatment ? Array.Empty() : chapters.Where(ShouldIncludeChapter); + // Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes) + IEnumerable retChapters; + if (libraryType == LibraryType.Book) + { + retChapters = Array.Empty(); + } else + { + retChapters = chapters + .Where(ShouldIncludeChapter); + } var storylineChapters = volumes - .WhereLooseLeaf() + .Where(v => v.Number == 0) .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) - .OrderBy(c => c.SortOrder) + .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) .ToList(); // When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter - if (storylineChapters.Count > 0) { - retChapters = retChapters.OrderBy(c => c.SortOrder, ChapterSortComparerDefaultLast.Default); + if (storylineChapters.Any()) { + retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } - return new SeriesDetailDto + return new SeriesDetailDto() { Specials = specials, Chapters = retChapters, @@ -586,7 +506,6 @@ public class SeriesService : ISeriesService StorylineChapters = storylineChapters, TotalCount = chapters.Count, UnreadCount = chapters.Count(c => c.Pages > 0 && c.PagesRead < c.Pages), - // TODO: See if we can get the ContinueFrom here }; } @@ -597,99 +516,72 @@ public class SeriesService : ISeriesService /// private static bool ShouldIncludeChapter(ChapterDto chapter) { - return !chapter.IsSpecial && chapter.MinNumber.IsNot(Parser.DefaultChapterNumber); + return !chapter.IsSpecial && !chapter.Number.Equals(Tasks.Scanner.Parser.Parser.DefaultChapter); } - /// - /// Should the volume be included and if so, this renames - /// - /// - /// - /// - /// - public static bool RenameVolumeName(VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") + public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") { - if (libraryType is LibraryType.Book or LibraryType.LightNovel) + if (libraryType == LibraryType.Book) { - var firstChapter = volume.Chapters.First(); - // On Books, skip volumes that are specials, since these will be shown - // if (firstChapter.IsSpecial) - // { - // // Some books can be SP marker and also position of 0, this will trick Kavita into rendering it as part of a non-special volume - // // We need to rename the entity so that it renders out correctly - // return false; - // } if (string.IsNullOrEmpty(firstChapter.TitleName)) { - if (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return false; + if (firstChapter.Range.Equals(Tasks.Scanner.Parser.Parser.DefaultVolume)) return; var title = Path.GetFileNameWithoutExtension(firstChapter.Range); - if (string.IsNullOrEmpty(title)) return false; - volume.Name += $" - {title}"; // OPDS smart list 7 (just pdfs) triggered this + if (string.IsNullOrEmpty(title)) return; + volume.Name += $" - {title}"; } - else if (!volume.IsLooseLeaf()) + else if (volume.Name != "0") { - // If the titleName has Volume inside it, let's just send that back? - volume.Name = firstChapter.TitleName; + volume.Name += $" - {firstChapter.TitleName}"; } + // else + // { + // volume.Name += $""; + // } - return !firstChapter.IsSpecial; + return; } - volume.Name = $"{volumeLabel.Trim()} {volume.Name}".Trim(); - return true; + volume.Name = $"{volumeLabel} {volume.Name}".Trim(); } - public async Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash) + public async Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash) { - if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null"); + if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null"); if (isSpecial) { - return Parser.CleanSpecialTitle(chapterTitle!); + return Tasks.Scanner.Parser.Parser.CleanSpecialTitle(chapterTitle); } var hashSpot = withHash ? "#" : string.Empty; - var baseChapter = libraryType switch + return libraryType switch { - LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle!), - LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterRange), - LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), - LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), - LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterRange), - LibraryType.Image => await _localizationService.Translate(userId, "chapter-num", chapterRange), + LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle), + LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), + LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle), _ => await _localizationService.Translate(userId, "chapter-num", ' ') }; - - if (!string.IsNullOrEmpty(chapterTitle) && libraryType != LibraryType.Book && chapterTitle != chapterRange) - { - baseChapter += " - " + chapterTitle; - } - - - return baseChapter; } public async Task FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true) { - return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); + return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash); } public async Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true) { - return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); + return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash); } - // TODO: Refactor this out and use FormatChapterTitle instead across library public async Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false) { var hashSpot = withHash ? "#" : string.Empty; return (libraryType switch { LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty), - LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty), - LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty), _ => await _localizationService.Translate(userId, "chapter-num", ' ') }).Trim(); @@ -707,7 +599,7 @@ public class SeriesService : ISeriesService } /// - /// Update the relations attached to the Series. Generates associated Sequel/Prequel pairs on target series. + /// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series. /// /// /// @@ -725,90 +617,14 @@ public class SeriesService : ISeriesService UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting); UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion); UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi); + UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel); + UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel); UpdateRelationForKind(dto.Editions, series.Relations.Where(r => r.RelationKind == RelationKind.Edition).ToList(), series, RelationKind.Edition); - UpdateRelationForKind(dto.Annuals, series.Relations.Where(r => r.RelationKind == RelationKind.Annual).ToList(), series, RelationKind.Annual); - - await UpdatePrequelSequelRelations(dto.Prequels, series, RelationKind.Prequel); - await UpdatePrequelSequelRelations(dto.Sequels, series, RelationKind.Sequel); if (!_unitOfWork.HasChanges()) return true; return await _unitOfWork.CommitAsync(); } - /// - /// Updates Prequel/Sequel relations and creates reciprocal relations on target series. - /// - /// List of target series IDs - /// The current series being updated - /// The relation kind (Prequel or Sequel) - private async Task UpdatePrequelSequelRelations(ICollection targetSeriesIds, Series series, RelationKind kind) - { - var existingRelations = series.Relations.Where(r => r.RelationKind == kind).ToList(); - - // Remove relations that are not in the new list - foreach (var relation in existingRelations.Where(relation => !targetSeriesIds.Contains(relation.TargetSeriesId))) - { - series.Relations.Remove(relation); - await RemoveReciprocalRelation(series.Id, relation.TargetSeriesId, GetOppositeRelationKind(kind)); - } - - // Add new relations - foreach (var targetSeriesId in targetSeriesIds) - { - if (series.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == targetSeriesId)) - continue; - - series.Relations.Add(new SeriesRelation - { - Series = series, - SeriesId = series.Id, - TargetSeriesId = targetSeriesId, - RelationKind = kind - }); - - await AddReciprocalRelation(series.Id, targetSeriesId, GetOppositeRelationKind(kind)); - } - - _unitOfWork.SeriesRepository.Update(series); - } - - private static RelationKind GetOppositeRelationKind(RelationKind kind) - { - return kind == RelationKind.Prequel ? RelationKind.Sequel : RelationKind.Prequel; - } - - private async Task AddReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) - { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); - if (targetSeries == null) return; - - if (targetSeries.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId)) - return; - - targetSeries.Relations.Add(new SeriesRelation - { - Series = targetSeries, - SeriesId = targetSeriesId, - TargetSeriesId = sourceSeriesId, - RelationKind = kind - }); - - _unitOfWork.SeriesRepository.Update(targetSeries); - } - - private async Task RemoveReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) - { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); - if (targetSeries == null) return; - - var relationToRemove = targetSeries.Relations.FirstOrDefault(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId); - if (relationToRemove != null) - { - targetSeries.Relations.Remove(relationToRemove); - _unitOfWork.SeriesRepository.Update(targetSeries); - } - } - /// /// Applies the provided list to the series. Adds new relations and removes deleted relations. @@ -833,7 +649,7 @@ public class SeriesService : ISeriesService r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) != null) continue; - series.Relations.Add(new SeriesRelation + series.Relations.Add(new SeriesRelation() { Series = series, SeriesId = series.Id, @@ -852,13 +668,11 @@ public class SeriesService : ISeriesService { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } - if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || - (series.Library.Type is LibraryType.Book or LibraryType.LightNovel)) + if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) { return _emptyExpectedChapter; } - const int minimumTimeDeltas = 3; var chapters = _unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId) .Where(c => !c.IsSpecial) .OrderBy(c => c.CreatedUtc) @@ -869,29 +683,30 @@ public class SeriesService : ISeriesService // Calculate the time differences between consecutive chapters var timeDifferences = new List(); DateTime? previousChapterTime = null; - foreach (var chapterCreatedUtc in chapters.Select(c => c.CreatedUtc)) + foreach (var chapter in chapters) { - if (previousChapterTime.HasValue && (chapterCreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) + if (previousChapterTime.HasValue && (chapter.CreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) { continue; // Skip this chapter if it's within an hour of the previous one } - if ((chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) + if ((chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) { - timeDifferences.Add(chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero); + timeDifferences.Add(chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero); } - previousChapterTime = chapterCreatedUtc; + previousChapterTime = chapter.CreatedUtc; } - if (timeDifferences.Count < minimumTimeDeltas) + + if (timeDifferences.Count < 3) { return _emptyExpectedChapter; } var historicalTimeDifferences = timeDifferences.Select(td => td.TotalDays).ToList(); - if (historicalTimeDifferences.Count < minimumTimeDeltas) + if (historicalTimeDifferences.Count < 3) { return _emptyExpectedChapter; } @@ -905,25 +720,21 @@ public class SeriesService : ISeriesService } // Calculate the forecast for when the next chapter is expected - // var nextChapterExpected = chapters.Count > 0 - // ? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(forecastedTimeDifference) - // : (DateTime?)null; - var lastChapterDate = chapters.Max(c => c.CreatedUtc); - var estimatedDate = lastChapterDate.AddDays(forecastedTimeDifference); - var nextChapterExpected = estimatedDate.Day > DateTime.DaysInMonth(estimatedDate.Year, estimatedDate.Month) - ? new DateTime(estimatedDate.Year, estimatedDate.Month, DateTime.DaysInMonth(estimatedDate.Year, estimatedDate.Month)) - : estimatedDate; + var nextChapterExpected = chapters.Any() + ? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(forecastedTimeDifference) + : (DateTime?)null; // For number and volume number, we need the highest chapter, not the latest created - var lastChapter = chapters.MaxBy(c => c.MaxNumber)!; - var lastChapterNumber = lastChapter.MaxNumber; + var lastChapter = chapters.MaxBy(c => c.Number.AsFloat())!; + float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, + out var lastChapterNumber); - var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max(); + var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max(); - var result = new NextExpectedChapterDto + var result = new NextExpectedChapterDto() { ChapterNumber = 0, - VolumeNumber = Parser.LooseLeafVolumeNumber, + VolumeNumber = 0, ExpectedDate = nextChapterExpected, Title = string.Empty }; @@ -931,21 +742,23 @@ public class SeriesService : ISeriesService if (lastChapterNumber > 0) { result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1; - result.VolumeNumber = lastChapter.Volume.MinNumber; + result.VolumeNumber = lastChapter.Volume.Number; result.Title = series.Library.Type switch { - LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), - LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), - LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), - LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), - LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), - _ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber) + LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", + new object[] {result.ChapterNumber}), + LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", + new object[] {"#", result.ChapterNumber}), + LibraryType.Book => await _localizationService.Translate(userId, "book-num", + new object[] {result.ChapterNumber}), + _ => "Chapter " + result.ChapterNumber }; } else { result.VolumeNumber = lastVolumeNum + 1; - result.Title = await _localizationService.Translate(userId, "volume-num", result.VolumeNumber); + result.Title = await _localizationService.Translate(userId, "volume-num", + new object[] {result.VolumeNumber}); } @@ -954,7 +767,7 @@ public class SeriesService : ISeriesService private static double ExponentialSmoothing(IList data, double alpha) { - var forecast = data[0]; + var forecast = data.First(); foreach (var value in data) { diff --git a/API/Services/SettingsService.cs b/API/Services/SettingsService.cs deleted file mode 100644 index fd44b5962..000000000 --- a/API/Services/SettingsService.cs +++ /dev/null @@ -1,447 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Logging; -using API.Services.Tasks.Scanner; -using Hangfire; -using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using Kavita.Common.Helpers; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface ISettingsService -{ - Task UpdateMetadataSettings(MetadataSettingsDto dto); - Task UpdateSettings(ServerSettingDto updateSettingsDto); -} - - -public class SettingsService : ISettingsService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly ILibraryWatcher _libraryWatcher; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - - public SettingsService(IUnitOfWork unitOfWork, IDirectoryService directoryService, - ILibraryWatcher libraryWatcher, ITaskScheduler taskScheduler, - ILogger logger) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _libraryWatcher = libraryWatcher; - _taskScheduler = taskScheduler; - _logger = logger; - } - - /// - /// Update the metadata settings for Kavita+ Metadata feature - /// - /// - /// - public async Task UpdateMetadataSettings(MetadataSettingsDto dto) - { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings(); - existingMetadataSetting.Enabled = dto.Enabled; - existingMetadataSetting.EnableSummary = dto.EnableSummary; - existingMetadataSetting.EnableLocalizedName = dto.EnableLocalizedName; - existingMetadataSetting.EnablePublicationStatus = dto.EnablePublicationStatus; - existingMetadataSetting.EnableRelationships = dto.EnableRelationships; - existingMetadataSetting.EnablePeople = dto.EnablePeople; - existingMetadataSetting.EnableStartDate = dto.EnableStartDate; - existingMetadataSetting.EnableGenres = dto.EnableGenres; - existingMetadataSetting.EnableTags = dto.EnableTags; - existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming; - existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage; - - existingMetadataSetting.EnableChapterPublisher = dto.EnableChapterPublisher; - existingMetadataSetting.EnableChapterSummary = dto.EnableChapterSummary; - existingMetadataSetting.EnableChapterTitle = dto.EnableChapterTitle; - existingMetadataSetting.EnableChapterReleaseDate = dto.EnableChapterReleaseDate; - existingMetadataSetting.EnableChapterCoverImage = dto.EnableChapterCoverImage; - - existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? []; - - existingMetadataSetting.Blacklist = (dto.Blacklist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? []; - existingMetadataSetting.Whitelist = (dto.Whitelist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? []; - existingMetadataSetting.Overrides = [.. dto.Overrides ?? []]; - existingMetadataSetting.PersonRoles = dto.PersonRoles ?? []; - - // Handle Field Mappings - - // Clear existing mappings - existingMetadataSetting.FieldMappings ??= []; - _unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings); - existingMetadataSetting.FieldMappings.Clear(); - - if (dto.FieldMappings != null) - { - // Add new mappings - foreach (var mappingDto in dto.FieldMappings) - { - existingMetadataSetting.FieldMappings.Add(new MetadataFieldMapping - { - SourceType = mappingDto.SourceType, - DestinationType = mappingDto.DestinationType, - SourceValue = mappingDto.SourceValue, - DestinationValue = mappingDto.DestinationValue, - ExcludeFromSource = mappingDto.ExcludeFromSource - }); - } - } - - // Save changes - await _unitOfWork.CommitAsync(); - - // Return updated settings - return await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - } - - /// - /// Update Server Settings - /// - /// - /// - /// - public async Task UpdateSettings(ServerSettingDto updateSettingsDto) - { - // We do not allow CacheDirectory changes, so we will ignore. - var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); - var updateBookmarks = false; - var originalBookmarkDirectory = _directoryService.BookmarkDirectory; - - var bookmarkDirectory = updateSettingsDto.BookmarksDirectory; - if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") && - !updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/")) - { - bookmarkDirectory = - _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); - } - - if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory)) - { - bookmarkDirectory = _directoryService.BookmarkDirectory; - } - - var updateTask = false; - foreach (var setting in currentSettings) - { - if (setting.Key == ServerSettingKey.OnDeckProgressDays && - updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.OnDeckUpdateDays && - updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) - { - if (OsInfo.IsDocker) continue; - setting.Value = updateSettingsDto.Port + string.Empty; - // Port is managed in appSetting.json - Configuration.Port = updateSettingsDto.Port; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.CacheSize && - updateSettingsDto.CacheSize + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.CacheSize + string.Empty; - // CacheSize is managed in appSetting.json - Configuration.CacheSize = updateSettingsDto.CacheSize; - _unitOfWork.SettingsRepository.Update(setting); - } - - updateTask = updateTask || UpdateSchedulingSettings(setting, updateSettingsDto); - - UpdateEmailSettings(setting, updateSettingsDto); - - - - if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value) - { - if (OsInfo.IsDocker) continue; - // Validate IP addresses - foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', - StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) - { - if (!IPAddress.TryParse(ipAddress.Trim(), out _)) - { - throw new KavitaException("ip-address-invalid"); - } - } - - setting.Value = updateSettingsDto.IpAddresses; - // IpAddresses is managed in appSetting.json - Configuration.IpAddresses = updateSettingsDto.IpAddresses; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) - { - var path = !updateSettingsDto.BaseUrl.StartsWith('/') - ? $"/{updateSettingsDto.BaseUrl}" - : updateSettingsDto.BaseUrl; - path = !path.EndsWith('/') - ? $"{path}/" - : path; - setting.Value = path; - Configuration.BaseUrl = updateSettingsDto.BaseUrl; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.LoggingLevel && - updateSettingsDto.LoggingLevel + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.LoggingLevel + string.Empty; - LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel); - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EnableOpds && - updateSettingsDto.EnableOpds + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.EnableOpds + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EncodeMediaAs && - ((int)updateSettingsDto.EncodeMediaAs).ToString() != setting.Value) - { - setting.Value = ((int)updateSettingsDto.EncodeMediaAs).ToString(); - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.CoverImageSize && - ((int)updateSettingsDto.CoverImageSize).ToString() != setting.Value) - { - setting.Value = ((int)updateSettingsDto.CoverImageSize).ToString(); - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value) - { - setting.Value = (updateSettingsDto.HostName + string.Empty).Trim(); - setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) - { - // Validate new directory can be used - if (!await _directoryService.CheckWriteAccess(bookmarkDirectory)) - { - throw new KavitaException("bookmark-dir-permissions"); - } - - originalBookmarkDirectory = setting.Value; - - // Normalize the path deliminators. Just to look nice in DB, no functionality - setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); - _unitOfWork.SettingsRepository.Update(setting); - updateBookmarks = true; - - } - - if (setting.Key == ServerSettingKey.AllowStatCollection && - updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.AllowStatCollection + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.TotalBackups && - updateSettingsDto.TotalBackups + string.Empty != setting.Value) - { - if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1) - { - throw new KavitaException("total-backups"); - } - - setting.Value = updateSettingsDto.TotalBackups + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.TotalLogs && - updateSettingsDto.TotalLogs + string.Empty != setting.Value) - { - if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1) - { - throw new KavitaException("total-logs"); - } - - setting.Value = updateSettingsDto.TotalLogs + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EnableFolderWatching && - updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - } - - if (!_unitOfWork.HasChanges()) return updateSettingsDto; - - try - { - await _unitOfWork.CommitAsync(); - - if (!updateSettingsDto.AllowStatCollection) - { - _taskScheduler.CancelStatsTasks(); - } - else - { - await _taskScheduler.ScheduleStatsTasks(); - } - - if (updateBookmarks) - { - UpdateBookmarkDirectory(originalBookmarkDirectory, bookmarkDirectory); - } - - if (updateTask) - { - BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks()); - } - - if (updateSettingsDto.EnableFolderWatching) - { - BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching()); - } - else - { - BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching()); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when updating server settings"); - await _unitOfWork.RollbackAsync(); - throw new KavitaException("generic-error"); - } - - - _logger.LogInformation("Server Settings updated"); - - return updateSettingsDto; - } - - private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory) - { - _directoryService.ExistOrCreate(bookmarkDirectory); - _directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); - _directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); - } - - private bool UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) - { - if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) - { - setting.Value = updateSettingsDto.TaskBackup; - _unitOfWork.SettingsRepository.Update(setting); - - return true; - } - - if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) - { - setting.Value = updateSettingsDto.TaskScan; - _unitOfWork.SettingsRepository.Update(setting); - return true; - } - - if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value) - { - setting.Value = updateSettingsDto.TaskCleanup; - _unitOfWork.SettingsRepository.Update(setting); - return true; - } - return false; - } - - private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) - { - if (setting.Key == ServerSettingKey.EmailHost && - updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailPort && - updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailAuthPassword && - updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailAuthUserName && - updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailSenderAddress && - updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailSenderDisplayName && - updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailSizeLimit && - updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailEnableSsl && - updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.EmailCustomizedTemplates && - updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value) - { - setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); - } - } -} diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index 006bad184..2c8552749 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -5,14 +5,10 @@ using System.Threading.Tasks; using API.Data; using API.DTOs; using API.DTOs.Statistics; -using API.DTOs.Stats; using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -36,7 +32,6 @@ public interface IStatisticService IEnumerable> GetWordsReadCountByYear(int userId = 0); Task UpdateServerStatistics(); Task TimeSpentReadingForUsersAsync(IList userIds, IList libraryIds); - Task> GetFilesByExtension(string fileExtension); } /// @@ -59,17 +54,13 @@ public class StatisticService : IStatisticService public async Task GetUserReadStatistics(int userId, IList libraryIds) { if (libraryIds.Count == 0) - { libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - } - // Total Pages Read var totalPagesRead = await _context.AppUserProgresses .Where(p => p.AppUserId == userId) .Where(p => libraryIds.Contains(p.LibraryId)) - .Select(p => (int?) p.PagesRead) - .SumAsync() ?? 0; + .SumAsync(p => p.PagesRead); var timeSpentReading = await TimeSpentReadingForUsersAsync(new List() {userId}, libraryIds); @@ -87,10 +78,10 @@ public class StatisticService : IStatisticService .CountAsync(); var lastActive = await _context.AppUserProgresses + .OrderByDescending(p => p.LastModified) .Where(p => p.AppUserId == userId) .Select(p => p.LastModified) - .DefaultIfEmpty() - .MaxAsync(); + .FirstOrDefaultAsync(); // First get the total pages per library @@ -112,42 +103,15 @@ public class StatisticService : IStatisticService }) .ToListAsync(); - - // New solution. Calculate total hours then divide by number of weeks from time account was created (or min reading event) till now - var averageReadingTimePerWeek = await _context.AppUserProgresses + var averageReadingTimePerWeek = _context.AppUserProgresses .Where(p => p.AppUserId == userId) .Join(_context.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new { - // TODO: See if this can be done in the DB layer - AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) * - ((float) c.AvgHoursToRead) + AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) * ((float) c.AvgHoursToRead) }) .Select(x => x.AverageReadingHours) - .SumAsync(); - - var earliestReadDate = await _context.AppUserProgresses - .Where(p => p.AppUserId == userId) - .Select(p => p.Created) - .DefaultIfEmpty() - .MinAsync(); - - if (earliestReadDate == DateTime.MinValue) - { - averageReadingTimePerWeek = 0; - } - else - { -#pragma warning disable S6561 - var timeDifference = DateTime.Now - earliestReadDate; -#pragma warning restore S6561 - var deltaWeeks = (int)Math.Ceiling(timeDifference.TotalDays / 7); - - averageReadingTimePerWeek /= deltaWeeks; - } - - - + .Average() * 7.0; return new UserReadStatistics() { @@ -293,6 +257,7 @@ public class StatisticService : IStatisticService var distinctPeople = _context.Person + .AsSplitQuery() .AsEnumerable() .GroupBy(sm => sm.NormalizedName) .Select(sm => sm.Key) @@ -310,7 +275,7 @@ public class StatisticService : IStatisticService TotalPeople = distinctPeople, TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes), TotalTags = await _context.Tag.CountAsync(), - VolumeCount = await _context.Volume.Where(v => Math.Abs(v.MinNumber - Parser.LooseLeafVolumeNumber) > 0.001f).CountAsync(), + VolumeCount = await _context.Volume.Where(v => v.Number != 0).CountAsync(), MostActiveUsers = mostActiveUsers, MostActiveLibraries = mostActiveLibrary, MostPopularSeries = mostPopularSeries, @@ -358,9 +323,8 @@ public class StatisticService : IStatisticService SeriesId = u.SeriesId, LibraryId = u.LibraryId, ReadDate = u.LastModified, - ReadDateUtc = u.LastModifiedUtc, ChapterId = u.ChapterId, - ChapterNumber = _context.Chapter.Single(c => c.Id == u.ChapterId).MinNumber + ChapterNumber = _context.Chapter.Single(c => c.Id == u.ChapterId).Number }) .OrderByDescending(d => d.ReadDate) .ToListAsync(); @@ -555,16 +519,6 @@ public class StatisticService : IStatisticService p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages)))); } - public async Task> GetFilesByExtension(string fileExtension) - { - var query = _context.MangaFile - .Where(f => f.Extension == fileExtension) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(f => f.FilePath); - - return await query.ToListAsync(); - } - public async Task> GetTopUsers(int days) { var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); @@ -587,6 +541,7 @@ public class StatisticService : IStatisticService .Contains(c.Id)) }) .OrderByDescending(d => d.Chapters.Sum(c => c.AvgHoursToRead)) + .Take(5) .ToList(); @@ -606,17 +561,16 @@ public class StatisticService : IStatisticService chapterLibLookup.Add(cl.ChapterId, cl.LibraryId); } - var user = new Dictionary>(); + var user = new Dictionary>(); foreach (var userChapter in topUsersAndReadChapters) { - if (!user.ContainsKey(userChapter.User.Id)) user.Add(userChapter.User.Id, []); + if (!user.ContainsKey(userChapter.User.Id)) user.Add(userChapter.User.Id, new Dictionary()); var libraryTimes = user[userChapter.User.Id]; foreach (var chapter in userChapter.Chapters) { var library = libraries.First(l => l.Id == chapterLibLookup[chapter.Id]); - libraryTimes.TryAdd(library.Type, 0f); - + if (!libraryTimes.ContainsKey(library.Type)) libraryTimes.Add(library.Type, 0L); var existingHours = libraryTimes[library.Type]; libraryTimes[library.Type] = existingHours + chapter.AvgHoursToRead; } @@ -629,8 +583,7 @@ public class StatisticService : IStatisticService { UserId = userId, Username = users.First(u => u.Id == userId).UserName, - BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0 + - (user[userId].TryGetValue(LibraryType.LightNovel, out var bookTime2) ? bookTime2 : 0), + BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0, ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0, MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0, }) diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs index 1f2e55579..a52fab767 100644 --- a/API/Services/StreamService.cs +++ b/API/Services/StreamService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -12,7 +11,6 @@ using API.Helpers; using API.SignalR; using Kavita.Common; using Kavita.Common.Helpers; -using Microsoft.Extensions.Logging; namespace API.Services; @@ -35,9 +33,7 @@ public interface IStreamService Task CreateExternalSource(int userId, ExternalSourceDto dto); Task UpdateExternalSource(int userId, ExternalSourceDto dto); Task DeleteExternalSource(int userId, int externalSourceId); - Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId); - Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId); - Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter); + } public class StreamService : IStreamService @@ -45,14 +41,12 @@ public class StreamService : IStreamService private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _eventHub; private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - public StreamService(IUnitOfWork unitOfWork, IEventHub eventHub, ILocalizationService localizationService, ILogger logger) + public StreamService(IUnitOfWork unitOfWork, IEventHub eventHub, ILocalizationService localizationService) { _unitOfWork = unitOfWork; _eventHub = eventHub; _localizationService = localizationService; - _logger = logger; } public async Task> GetDashboardStreams(int userId, bool visibleOnly = true) @@ -78,7 +72,7 @@ public class StreamService : IStreamService var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); var maxOrder = user!.DashboardStreams.Max(d => d.Order); @@ -98,7 +92,6 @@ public class StreamService : IStreamService var ret = new DashboardStreamDto() { - Id = createdStream.Id, Name = createdStream.Name, IsProvided = createdStream.IsProvided, Visible = createdStream.Visible, @@ -131,10 +124,7 @@ public class StreamService : IStreamService AppUserIncludes.DashboardStreams); var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id); if (stream == null) - { throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); - } - if (stream.Order == dto.ToPosition) return; var list = user!.DashboardStreams.OrderBy(s => s.Order).ToList(); @@ -170,7 +160,7 @@ public class StreamService : IStreamService var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + var stream = user?.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); var maxOrder = user!.SideNavStreams.Max(d => d.Order); @@ -190,7 +180,6 @@ public class StreamService : IStreamService var ret = new SideNavStreamDto() { - Id = createdStream.Id, Name = createdStream.Name, IsProvided = createdStream.IsProvided, Visible = createdStream.Visible, @@ -354,71 +343,5 @@ public class StreamService : IStreamService await _unitOfWork.CommitAsync(); } - public async Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetSideNavStream(sideNavStreamId); - if (stream == null) throw new KavitaException("sidenav-stream-doesnt-exist"); - if (stream.AppUserId != userId) throw new KavitaException("sidenav-stream-doesnt-exist"); - - - if (stream.StreamType != SideNavStreamType.SmartFilter) - { - throw new KavitaException("sidenav-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting SideNav Smart Filter Stream: {FilterId}", sideNavStreamId); - throw; - } - } - - public async Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetDashboardStream(dashboardStreamId); - if (stream == null) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.AppUserId != userId) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.StreamType != DashboardStreamType.SmartFilter) - { - throw new KavitaException("dashboard-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting Dashboard Smart Filter Stream: {FilterId}", dashboardStreamId); - throw; - } - } - - public async Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter) - { - var sideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(smartFilter.Id); - var dashboardStreams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(smartFilter.Id); - - foreach (var sideNavStream in sideNavStreams) - { - sideNavStream.Name = smartFilter.Name; - } - - foreach (var dashboardStream in dashboardStreams) - { - dashboardStream.Name = smartFilter.Name; - } - - await _unitOfWork.CommitAsync(); - } } diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 7cba28695..3494f0bf6 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -9,16 +9,14 @@ using System.Linq; using API.Comparators; using API.Entities; using API.Extensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using Microsoft.Extensions.Logging; namespace API.Services; -#nullable enable public interface ITachiyomiService { - Task GetLatestChapter(int seriesId, int userId); + Task GetLatestChapter(int seriesId, int userId); Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber); } @@ -30,12 +28,12 @@ public class TachiyomiService : ITachiyomiService { private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IReaderService _readerService; private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US"); - public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) + public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) { _unitOfWork = unitOfWork; _readerService = readerService; @@ -52,7 +50,7 @@ public class TachiyomiService : ITachiyomiService /// If its a chapter, return the chapterDto as is. /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes - public async Task GetLatestChapter(int seriesId, int userId) + public async Task GetLatestChapter(int seriesId, int userId) { var currentChapter = await _readerService.GetContinuePoint(seriesId, userId); @@ -70,53 +68,55 @@ public class TachiyomiService : ITachiyomiService // Else return the max chapter to Tachiyomi so it can consider everything read var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); - var looseLeafChapterVolume = volumes.GetLooseLeafVolumeOrDefault(); + var looseLeafChapterVolume = volumes.Find(v => v.Number == 0); if (looseLeafChapterVolume == null) { var volumeChapter = _mapper.Map(volumes - [^1].Chapters - .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default) + .Last().Chapters + .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) .Last()); - - if (volumeChapter.MinNumber.Is(Parser.LooseLeafVolumeNumber)) + if (volumeChapter.Number == "0") { var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); - return CreateTachiyomiChapterDto(volume.MinNumber); + return new ChapterDto() + { + // Use R to ensure that localization of underlying system doesn't affect the stringification + // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework + Number = (volume.Number / 10_000f).ToString("R", EnglishCulture) + }; } - return CreateTachiyomiChapterDto(volumeChapter.MinNumber); + return new ChapterDto() + { + Number = (int.Parse(volumeChapter.Number) / 10_000f).ToString("R", EnglishCulture) + }; } var lastChapter = looseLeafChapterVolume.Chapters - .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) + .OrderBy(c => double.Parse(c.Number, CultureInfo.InvariantCulture), ChapterSortComparer.Default) .Last(); - - return _mapper.Map(lastChapter); + return _mapper.Map(lastChapter); } // There is progress, we now need to figure out the highest volume or chapter and return that. var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!; - var volumeWithProgress = (await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId))!; + var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); // We only encode for single-file volumes - if (!volumeWithProgress.IsLooseLeaf() && volumeWithProgress.Chapters.Count == 1) + if (volumeWithProgress!.Number != 0 && volumeWithProgress.Chapters.Count == 1) { // The progress is on a volume, encode it as a fake chapterDTO - return CreateTachiyomiChapterDto(volumeWithProgress.MinNumber); + return new ChapterDto() + { + // Use R to ensure that localization of underlying system doesn't affect the stringification + // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework + Number = (volumeWithProgress.Number / 10_000f).ToString("R", EnglishCulture) + + }; } // Progress is just on a chapter, return as is - return _mapper.Map(prevChapter); - } - - private static TachiyomiChapterDto CreateTachiyomiChapterDto(float number) - { - return new TachiyomiChapterDto() - { - // Use R to ensure that localization of underlying system doesn't affect the stringification - // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework - Number = (number / 10_000f).ToString("R", EnglishCulture) - }; + return prevChapter; } /// diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 575f89b3b..a7a309844 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -4,17 +4,12 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using API.Data; -using API.Data.Repositories; using API.Entities.Enums; -using API.Extensions; -using API.Helpers; using API.Helpers.Converters; using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Metadata; -using API.SignalR; using Hangfire; -using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services; @@ -24,22 +19,23 @@ public interface ITaskScheduler Task ScheduleTasks(); Task ScheduleStatsTasks(); void ScheduleUpdaterTasks(); - Task ScheduleKavitaPlusTasks(); - void ScanFolder(string folderPath, string originalPath, TimeSpan delay); + void ScanFolder(string folderPath, TimeSpan delay); void ScanFolder(string folderPath); - Task ScanLibrary(int libraryId, bool force = false); - Task ScanLibraries(bool force = false); + void ScanLibrary(int libraryId, bool force = false); + void ScanLibraries(bool force = false); void CleanupChapters(int[] chapterIds); - void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); - void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); - Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); + void RefreshMetadata(int libraryId, bool forceUpdate = true); + void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false); + void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); + void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false); void CancelStatsTasks(); Task RunStatCollection(); + void ScanSiteThemes(); void CovertAllCoversToEncoding(); Task CleanupDbEntries(); - Task CheckForUpdate(); - Task SyncThemes(); + Task ScrobbleUpdates(int userId); + } public class TaskScheduler : ITaskScheduler { @@ -59,34 +55,25 @@ public class TaskScheduler : ITaskScheduler private readonly IMediaConversionService _mediaConversionService; private readonly IScrobblingService _scrobblingService; private readonly ILicenseService _licenseService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly ISmartCollectionSyncService _smartCollectionSyncService; - private readonly IWantToReadSyncService _wantToReadSyncService; - private readonly IEventHub _eventHub; public static BackgroundJobServer Client => new (); public const string ScanQueue = "scan"; public const string DefaultQueue = "default"; public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; - public const string SyncThemesTaskId = "sync-themes"; public const string CheckForUpdateId = "check-updates"; public const string CleanupDbTaskId = "cleanup-db"; public const string CleanupTaskId = "cleanup"; public const string BackupTaskId = "backup"; public const string ScanLibrariesTaskId = "scan-libraries"; public const string ReportStatsTaskId = "report-stats"; - public const string CheckScrobblingTokensId = "check-scrobbling-tokens"; - public const string ProcessScrobblingEventsId = "process-scrobbling-events"; - public const string ProcessProcessedScrobblingEventsId = "process-processed-scrobbling-events"; - public const string LicenseCheckId = "license-check"; - public const string KavitaPlusDataRefreshId = "kavita+-data-refresh"; - public const string KavitaPlusStackSyncId = "kavita+-stack-sync"; - public const string KavitaPlusWantToReadSyncId = "kavita+-want-to-read-sync"; + public const string CheckScrobblingTokens = "check-scrobbling-tokens"; + public const string ProcessScrobblingEvents = "process-scrobbling-events"; + public const string ProcessProcessedScrobblingEvents = "process-processed-scrobbling-events"; + public const string LicenseCheck = "license-check"; - public static readonly ImmutableArray ScanTasks = - ["ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"]; - private static readonly ImmutableArray NonCronOptions = ["disabled", "daily", "weekly"]; + private static readonly ImmutableArray ScanTasks = + ImmutableArray.Create("ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"); private static readonly Random Rnd = new Random(); @@ -100,9 +87,7 @@ public class TaskScheduler : ITaskScheduler IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, - IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, - IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, - IWantToReadSyncService wantToReadSyncService, IEventHub eventHub) + IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService) { _cacheService = cacheService; _logger = logger; @@ -119,41 +104,27 @@ public class TaskScheduler : ITaskScheduler _mediaConversionService = mediaConversionService; _scrobblingService = scrobblingService; _licenseService = licenseService; - _externalMetadataService = externalMetadataService; - _smartCollectionSyncService = smartCollectionSyncService; - _wantToReadSyncService = wantToReadSyncService; - _eventHub = eventHub; } public async Task ScheduleTasks() { _logger.LogInformation("Scheduling reoccurring tasks"); - var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Value; - if (IsInvalidCronSetting(setting)) - { - _logger.LogError("Scan Task has invalid cron, defaulting to Daily"); - RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), - Cron.Daily, RecurringJobOptions); - } - else + if (setting != null) { var scanLibrarySetting = setting; _logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting); - RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), + RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(false), () => CronConverter.ConvertToCronNotation(scanLibrarySetting), RecurringJobOptions); } - + else + { + RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, RecurringJobOptions); + } setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; - if (IsInvalidCronSetting(setting)) - { - _logger.LogError("Backup Task has invalid cron, defaulting to Weekly"); - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), - Cron.Weekly, RecurringJobOptions); - } - else + if (setting != null) { _logger.LogDebug("Scheduling Backup Task for {Setting}", setting); var schedule = CronConverter.ConvertToCronNotation(setting); @@ -162,92 +133,27 @@ public class TaskScheduler : ITaskScheduler // Override daily and make 2am so that everything on system has cleaned up and no blocking schedule = Cron.Daily(2); } - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), - () => schedule, RecurringJobOptions); - } - - setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value; - if (IsInvalidCronSetting(setting)) - { - _logger.LogError("Cleanup Task has invalid cron, defaulting to Daily"); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), - Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => schedule, RecurringJobOptions); } else { - _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), - CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions); } + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions); - RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), - Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), - Cron.Monthly, RecurringJobOptions); - - RecurringJob.AddOrUpdate(SyncThemesTaskId, () => SyncThemes(), - Cron.Daily, RecurringJobOptions); - - await ScheduleKavitaPlusTasks(); - } - - private static bool IsInvalidCronSetting(string setting) - { - return setting == null || (!NonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting)); - } - - public async Task ScheduleKavitaPlusTasks() - { // KavitaPlus based (needs license check) - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license)) - { - return; - } - - RecurringJob.AddOrUpdate(CheckScrobblingTokensId, () => _scrobblingService.CheckExternalAccessTokens(), - Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(CheckScrobblingTokens, () => _scrobblingService.CheckExternalAccessTokens(), Cron.Daily, RecurringJobOptions); BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens()); // We also kick off an immediate check on startup + RecurringJob.AddOrUpdate(LicenseCheck, () => _licenseService.ValidateLicenseStatus(), LicenseService.Cron, RecurringJobOptions); + BackgroundJob.Enqueue(() => _licenseService.ValidateLicenseStatus()); - // Get the License Info (and cache it) on first load. This will internally cache the Github releases for the Version Service - await _licenseService.GetLicenseInfo(true); // Kick this off first to cache it then let it refresh every 9 hours (8 hour cache) - RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false), - LicenseService.Cron, RecurringJobOptions); - - // KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+ - RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), - Cron.Hourly(Rnd.Next(0, 60)), RecurringJobOptions); - RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), - Cron.Daily, RecurringJobOptions); - - // Backfilling/Freshening Reviews/Rating/Recommendations - RecurringJob.AddOrUpdate(KavitaPlusDataRefreshId, - () => _externalMetadataService.FetchExternalDataTask(), Cron.Daily(Rnd.Next(1, 5)), - RecurringJobOptions); - - // This shouldn't be so close to fetching data due to Rate limit concerns - RecurringJob.AddOrUpdate(KavitaPlusStackSyncId, - () => _smartCollectionSyncService.Sync(), Cron.Daily(Rnd.Next(6, 10)), - RecurringJobOptions); - - RecurringJob.AddOrUpdate(KavitaPlusWantToReadSyncId, - () => _wantToReadSyncService.Sync(), Cron.Weekly(DayOfWeekHelper.Random()), - RecurringJobOptions); - } - - /// - /// Removes any Kavita+ Recurring Jobs - /// - public static void RemoveKavitaPlusTasks() - { - RecurringJob.RemoveIfExists(CheckScrobblingTokensId); - RecurringJob.RemoveIfExists(LicenseCheckId); - RecurringJob.RemoveIfExists(ProcessScrobblingEventsId); - RecurringJob.RemoveIfExists(ProcessProcessedScrobblingEventsId); - RecurringJob.RemoveIfExists(KavitaPlusDataRefreshId); - RecurringJob.RemoveIfExists(KavitaPlusStackSyncId); - RecurringJob.RemoveIfExists(KavitaPlusWantToReadSyncId); + // KavitaPlus Scrobbling (every 4 hours) + RecurringJob.AddOrUpdate(ProcessScrobblingEvents, () => _scrobblingService.ProcessUpdatesSinceLastSync(), "0 */4 * * *", RecurringJobOptions); + RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEvents, () => _scrobblingService.ClearProcessedEvents(), Cron.Daily, RecurringJobOptions); } #region StatsTasks @@ -255,7 +161,7 @@ public class TaskScheduler : ITaskScheduler public async Task ScheduleStatsTasks() { - var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; + var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; if (!allowStatCollection) { _logger.LogDebug("User has opted out of stat collection, not registering tasks"); @@ -266,6 +172,10 @@ public class TaskScheduler : ITaskScheduler RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), RecurringJobOptions); } + public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false) + { + BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate)); + } /// /// Upon cancelling stat, we do report to the Stat service that we are no longer going to be reporting @@ -292,6 +202,18 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Schedule(() => _statsService.Send(), DateTimeOffset.Now.AddDays(1)); } + public void ScanSiteThemes() + { + if (HasAlreadyEnqueuedTask("ThemeService", "Scan", Array.Empty(), ScanQueue)) + { + _logger.LogInformation("A Theme Scan is already running"); + return; + } + + _logger.LogInformation("Enqueueing Site Theme scan"); + BackgroundJob.Enqueue(() => _themeService.Scan()); + } + public void CovertAllCoversToEncoding() { var defaultParams = Array.Empty(); @@ -315,44 +237,32 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Enqueue(() => CheckForUpdate()); } - /// - /// Queue up a Scan folder for a folder from Library Watcher. - /// - /// - /// - /// - public void ScanFolder(string folderPath, string originalPath, TimeSpan delay) + public void ScanFolder(string folderPath, TimeSpan delay) { var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); - var normalizedOriginal = Tasks.Scanner.Parser.Parser.NormalizePath(originalPath); - - if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, normalizedOriginal]) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) + if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", new object[] { normalizedFolder })) { - _logger.LogTrace("Skipped scheduling ScanFolder for {Folder} as a job already queued", + _logger.LogInformation("Skipped scheduling ScanFolder for {Folder} as a job already queued", normalizedFolder); return; } - // Not sure where we should put this code, but we can get a bunch of ScanFolders when original has slight variations, like - // create a folder, add a new file, etc. All of these can be merged into just 1 request. - _logger.LogInformation("Scheduling ScanFolder for {Folder}", normalizedFolder); - BackgroundJob.Schedule(() => _scannerService.ScanFolder(normalizedFolder, normalizedOriginal), delay); + BackgroundJob.Schedule(() => _scannerService.ScanFolder(normalizedFolder), delay); } public void ScanFolder(string folderPath) { var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); - if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) + if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", new object[] {normalizedFolder})) { - _logger.LogTrace("Skipped scheduling ScanFolder for {Folder} as a job already queued", + _logger.LogInformation("Skipped scheduling ScanFolder for {Folder} as a job already queued", normalizedFolder); return; } _logger.LogInformation("Scheduling ScanFolder for {Folder}", normalizedFolder); - _scannerService.ScanFolder(normalizedFolder, string.Empty); + _scannerService.ScanFolder(normalizedFolder); } #endregion @@ -362,25 +272,32 @@ public class TaskScheduler : ITaskScheduler await _cleanupService.CleanupDbEntries(); } + /// + /// TODO: Remove this for Release + /// + /// + public async Task ScrobbleUpdates(int userId) + { + if (!await _licenseService.HasActiveLicense()) return; + BackgroundJob.Enqueue(() => _scrobblingService.ProcessUpdatesSinceLastSync()); + } + /// /// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future. /// /// - public async Task ScanLibraries(bool force = false) + public void ScanLibraries(bool force = false) { if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) { _logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours"); - // Send InfoEvent to UI as this is invoked my API BackgroundJob.Schedule(() => ScanLibraries(force), TimeSpan.FromHours(3)); - await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"Scan libraries task delayed", - $"A scan was ongoing during processing of the scan libraries task. Task has been rescheduled for 3 hours: {DateTime.Now.AddHours(3)}")); return; } BackgroundJob.Enqueue(() => _scannerService.ScanLibraries(force)); } - public async Task ScanLibrary(int libraryId, bool force = false) + public void ScanLibrary(int libraryId, bool force = false) { if (HasScanTaskRunningForLibrary(libraryId)) { @@ -389,18 +306,15 @@ public class TaskScheduler : ITaskScheduler } if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); - _logger.LogInformation("A Scan is already running, rescheduling ScanLibrary in 3 hours"); - await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"Scan library task delayed", - $"A scan was ongoing during processing of the {library!.Name} scan task. Task has been rescheduled for 3 hours: {DateTime.Now.AddHours(3)}")); + _logger.LogInformation("A Library Scan is already running, rescheduling ScanLibrary in 3 hours"); BackgroundJob.Schedule(() => ScanLibrary(libraryId, force), TimeSpan.FromHours(3)); return; } _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); - var jobId = BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, force, true)); + BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, force)); // When we do a scan, force cache to re-unpack in case page numbers change - BackgroundJob.ContinueJobWith(jobId, () => _cleanupService.CleanupCacheDirectory()); + BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheAndTempDirectories()); } public void TurnOnScrobbling(int userId = 0) @@ -413,12 +327,12 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); } - public void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true) + public void RefreshMetadata(int libraryId, bool forceUpdate = true) { var alreadyEnqueued = HasAlreadyEnqueuedTask(MetadataService.Name, "GenerateCoversForLibrary", - [libraryId, true, true]) || + new object[] {libraryId, true}) || HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary", - [libraryId, false, false]); + new object[] {libraryId, false}); if (alreadyEnqueued) { _logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping"); @@ -426,35 +340,31 @@ public class TaskScheduler : ITaskScheduler } _logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId); - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, forceUpdate, forceColorscape)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, forceUpdate)); } - public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false) + public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false) { - if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", [libraryId, seriesId, forceUpdate, forceColorscape])) + if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", new object[] {libraryId, seriesId, forceUpdate})) { _logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping"); return; } _logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId); - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, seriesId, forceUpdate, forceColorscape)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, seriesId, forceUpdate)); } - public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false) + public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false) { - if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", [seriesId, forceUpdate], ScanQueue)) + if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, forceUpdate}, ScanQueue)) { _logger.LogInformation("A duplicate request to scan series occured. Skipping"); return; } if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) { - // BUG: This can end up triggering a ton of scan series calls (but i haven't seen in practice) - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); _logger.LogInformation("A Scan is already running, rescheduling ScanSeries in 10 minutes"); - await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"Scan series task delayed: {series!.Name}", - $"A scan was ongoing during processing of the scan series task. Task has been rescheduled for 10 minutes: {DateTime.Now.AddMinutes(10)}")); BackgroundJob.Schedule(() => ScanSeries(libraryId, seriesId, forceUpdate), TimeSpan.FromMinutes(10)); return; } @@ -463,15 +373,9 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Enqueue(() => _scannerService.ScanSeries(seriesId, forceUpdate)); } - /// - /// Calculates TimeToRead and bytes - /// - /// - /// - /// public void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false) { - if (HasAlreadyEnqueuedTask("WordCountAnalyzerService", "ScanSeries", [libraryId, seriesId, forceUpdate])) + if (HasAlreadyEnqueuedTask("WordCountAnalyzerService", "ScanSeries", new object[] {libraryId, seriesId, forceUpdate})) { _logger.LogInformation("A duplicate request to scan series occured. Skipping"); return; @@ -492,11 +396,6 @@ public class TaskScheduler : ITaskScheduler await _versionUpdaterService.PushUpdate(update); } - public async Task SyncThemes() - { - await _themeService.SyncThemes(); - } - /// /// If there is an enqueued or scheduled task for method /// @@ -506,14 +405,8 @@ public class TaskScheduler : ITaskScheduler public static bool HasScanTaskRunningForLibrary(int libraryId, bool checkRunningJobs = true) { return - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", [libraryId, true, true], ScanQueue, - checkRunningJobs) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", [libraryId, false, true], ScanQueue, - checkRunningJobs) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", [libraryId, true, false], ScanQueue, - checkRunningJobs) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", [libraryId, false, false], ScanQueue, - checkRunningJobs); + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, true}, ScanQueue, checkRunningJobs) || + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, false}, ScanQueue, checkRunningJobs); } /// @@ -525,11 +418,10 @@ public class TaskScheduler : ITaskScheduler public static bool HasScanTaskRunningForSeries(int seriesId, bool checkRunningJobs = true) { return - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", [seriesId, true], ScanQueue, checkRunningJobs) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", [seriesId, false], ScanQueue, checkRunningJobs); + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, true}, ScanQueue, checkRunningJobs) || + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, false}, ScanQueue, checkRunningJobs); } - /// /// Checks if this same invocation is already enqueued or scheduled /// @@ -538,7 +430,6 @@ public class TaskScheduler : ITaskScheduler /// object[] of arguments in the order they are passed to enqueued job /// Queue to check against. Defaults to "default" /// Check against running jobs. Defaults to false. - /// Check against arguments. Defaults to true. /// public static bool HasAlreadyEnqueuedTask(string className, string methodName, object[] args, string queue = DefaultQueue, bool checkRunningJobs = false) { @@ -570,7 +461,6 @@ public class TaskScheduler : ITaskScheduler return false; } - /// /// Checks against any jobs that are running or about to run /// diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index e2ed61ba1..3b1f7746c 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -9,11 +9,9 @@ using API.Entities.Enums; using API.Logging; using API.SignalR; using Hangfire; -using Kavita.Common.EnvironmentInfo; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -#nullable enable public interface IBackupService { @@ -45,6 +43,8 @@ public class BackupService : IBackupService _backupFiles = new List() { "appsettings.json", + "Hangfire.db", // This is not used atm + "Hangfire-log.db", // This is not used atm "kavita.db", "kavita.db-shm", // This wont always be there "kavita.db-wal" // This wont always be there @@ -90,7 +90,7 @@ public class BackupService : IBackupService await SendProgress(0.1F, "Copying core files"); var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace("/", "_").Replace(":", "_"); - var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}_v{BuildInfo.Version}.zip"); + var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip"); if (File.Exists(zipPath)) { @@ -104,29 +104,22 @@ public class BackupService : IBackupService _directoryService.ExistOrCreate(tempDirectory); _directoryService.ClearDirectory(tempDirectory); - await SendProgress(0.1F, "Copying config files"); _directoryService.CopyFilesToDirectory( - _backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)), tempDirectory); + _backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)).ToList(), tempDirectory); - // Copy any csv's as those are used for manual migrations - _directoryService.CopyFilesToDirectory( - _directoryService.GetFilesWithCertainExtensions(_directoryService.ConfigDirectory, @"\.csv"), tempDirectory); - - await SendProgress(0.2F, "Copying logs"); CopyLogsToBackupDirectory(tempDirectory); await SendProgress(0.25F, "Copying cover images"); + await CopyCoverImagesToBackupDirectory(tempDirectory); - await SendProgress(0.35F, "Copying templates images"); - CopyTemplatesToBackupDirectory(tempDirectory); - await SendProgress(0.5F, "Copying bookmarks"); + await CopyBookmarksToBackupDirectory(tempDirectory); await SendProgress(0.75F, "Copying themes"); - CopyThemesToBackupDirectory(tempDirectory); + CopyThemesToBackupDirectory(tempDirectory); await SendProgress(0.85F, "Copying favicons"); CopyFaviconsToBackupDirectory(tempDirectory); @@ -155,11 +148,6 @@ public class BackupService : IBackupService _directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); } - private void CopyTemplatesToBackupDirectory(string tempDirectory) - { - _directoryService.CopyDirectoryToDirectory(_directoryService.TemplateDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "templates")); - } - private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) { var outputTempDir = Path.Join(tempDirectory, "covers"); @@ -179,10 +167,6 @@ public class BackupService : IBackupService _directoryService.CopyFilesToDirectory( chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - var volumeImages = await _unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync(); - _directoryService.CopyFilesToDirectory( - volumeImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync(); _directoryService.CopyFilesToDirectory( libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index e39600c3f..257103708 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -8,38 +8,28 @@ using API.DTOs.Filtering; using API.Entities; using API.Entities.Enums; using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -#nullable enable public interface ICleanupService { Task Cleanup(); Task CleanupDbEntries(); void CleanupCacheAndTempDirectories(); - void CleanupCacheDirectory(); Task DeleteSeriesCoverImages(); Task DeleteChapterCoverImages(); Task DeleteTagCoverImages(); Task CleanupBackups(); Task CleanupLogs(); void CleanupTemp(); - Task EnsureChapterProgressIsCapped(); /// /// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled. /// /// Task CleanupWantToRead(); - - Task ConsolidateProgress(); - - Task CleanupMediaErrors(); - } /// /// Cleans up after operations on reoccurring basis @@ -81,23 +71,13 @@ public class CleanupService : ICleanupService _logger.LogInformation("Starting Cleanup"); await SendProgress(0F, "Starting cleanup"); - _logger.LogInformation("Cleaning temp directory"); _directoryService.ClearDirectory(_directoryService.TempDirectory); - await SendProgress(0.1F, "Cleaning temp directory"); CleanupCacheAndTempDirectories(); - await SendProgress(0.25F, "Cleaning old database backups"); _logger.LogInformation("Cleaning old database backups"); await CleanupBackups(); - - await SendProgress(0.35F, "Consolidating Progress Events"); - await ConsolidateProgress(); - - await SendProgress(0.4F, "Consolidating Media Errors"); - await CleanupMediaErrors(); - await SendProgress(0.50F, "Cleaning deleted cover images"); _logger.LogInformation("Cleaning deleted cover images"); await DeleteSeriesCoverImages(); @@ -108,10 +88,6 @@ public class CleanupService : ICleanupService await DeleteReadingListCoverImages(); await SendProgress(0.8F, "Cleaning old logs"); await CleanupLogs(); - await SendProgress(0.9F, "Cleaning progress events that exceed 100%"); - await EnsureChapterProgressIsCapped(); - await SendProgress(0.95F, "Cleaning abandoned database rows"); - await CleanupDbEntries(); await SendProgress(1F, "Cleanup finished"); _logger.LogInformation("Cleanup finished"); } @@ -124,7 +100,7 @@ public class CleanupService : ICleanupService await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); await _unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries(); } @@ -196,23 +172,6 @@ public class CleanupService : ICleanupService _logger.LogInformation("Cache and temp directory purged"); } - public void CleanupCacheDirectory() - { - _logger.LogInformation("Performing cleanup of Cache directories"); - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.CacheDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Cache directory purged"); - } - /// /// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept. /// @@ -243,108 +202,6 @@ public class CleanupService : ICleanupService _logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); } - /// - /// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one. - /// - public async Task ConsolidateProgress() - { - _logger.LogInformation("Consolidating Progress Events"); - // AppUserProgress - var allProgress = await _unitOfWork.AppUserProgressRepository.GetAllProgress(); - - // Group by the unique identifiers that would make a progress entry unique - var duplicateGroups = allProgress - .GroupBy(p => new - { - p.AppUserId, - p.ChapterId, - }) - .Where(g => g.Count() > 1); - - foreach (var group in duplicateGroups) - { - // Find the entry with the highest pages read - var highestProgress = group - .OrderByDescending(p => p.PagesRead) - .ThenByDescending(p => p.LastModifiedUtc) - .First(); - - // Get the duplicate entries to remove (all except the highest progress) - var duplicatesToRemove = group - .Where(p => p.Id != highestProgress.Id) - .ToList(); - - // Copy over any non-null BookScrollId if the highest progress entry doesn't have one - if (string.IsNullOrEmpty(highestProgress.BookScrollId)) - { - var firstValidScrollId = duplicatesToRemove - .FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId)) - ?.BookScrollId; - - if (firstValidScrollId != null) - { - highestProgress.BookScrollId = firstValidScrollId; - highestProgress.MarkModified(); - } - } - - // Remove the duplicates - foreach (var duplicate in duplicatesToRemove) - { - _unitOfWork.AppUserProgressRepository.Remove(duplicate); - } - } - - // Save changes - await _unitOfWork.CommitAsync(); - } - - /// - /// Scans through Media Error and removes any entries that have been fixed and are within the DB (proper files where wordcount/pagecount > 0) - /// - public async Task CleanupMediaErrors() - { - try - { - List errorStrings = ["This archive cannot be read or not supported", "File format not supported"]; - var mediaErrors = await _unitOfWork.MediaErrorRepository.GetAllErrorsAsync(errorStrings); - _logger.LogInformation("Beginning consolidation of {Count} Media Errors", mediaErrors.Count); - - var pathToErrorMap = mediaErrors - .GroupBy(me => Parser.NormalizePath(me.FilePath)) - .ToDictionary( - group => group.Key, - group => group.ToList() // The same file can be duplicated (rare issue when network drives die out midscan) - ); - - var normalizedPaths = pathToErrorMap.Keys.ToList(); - - // Find all files that are valid - var validFiles = await _unitOfWork.DataContext.MangaFile - .Where(f => normalizedPaths.Contains(f.FilePath) && f.Pages > 0) - .Select(f => f.FilePath) - .ToListAsync(); - - var removalCount = 0; - foreach (var validFilePath in validFiles) - { - if (!pathToErrorMap.TryGetValue(validFilePath, out var mediaError)) continue; - - _unitOfWork.MediaErrorRepository.Remove(mediaError); - removalCount++; - } - - await _unitOfWork.CommitAsync(); - - _logger.LogInformation("Finished consolidation of {Count} Media Errors, Removed: {RemovalCount}", - mediaErrors.Count, removalCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception consolidating media errors"); - } - } - public async Task CleanupLogs() { _logger.LogInformation("Performing cleanup of logs directory"); @@ -385,17 +242,6 @@ public class CleanupService : ICleanupService _logger.LogInformation("Temp directory purged"); } - /// - /// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count. - /// - /// - public async Task EnsureChapterProgressIsCapped() - { - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count"); - await _unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages(); - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete"); - } - /// /// This does not cleanup any Series that are not Completed or Cancelled /// @@ -425,8 +271,8 @@ public class CleanupService : ICleanupService var seriesIds = series.Select(s => s.Id).ToList(); if (seriesIds.Count == 0) continue; - user.WantToRead ??= new List(); - user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList(); + user.WantToRead ??= new List(); + user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.Id)).ToList(); _unitOfWork.UserRepository.Update(user); } diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs deleted file mode 100644 index 015613965..000000000 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ /dev/null @@ -1,740 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.SignalR; -using EasyCaching.Core; -using Flurl; -using Flurl.Http; -using HtmlAgilityPack; -using Kavita.Common; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NetVips; - - -namespace API.Services.Tasks.Metadata; -#nullable enable - -public interface ICoverDbService -{ - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url); - Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false); - Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false); -} - - -public class CoverDbService : ICoverDbService -{ - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IEasyCachingProviderFactory _cacheFactory; - private readonly IHostEnvironment _env; - private readonly IImageService _imageService; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private TimeSpan _cacheTime = TimeSpan.FromDays(10); - - private const string NewHost = "https://www.kavitareader.com/CoversDB/"; - - private static readonly string[] ValidIconRelations = { - "icon", - "apple-touch-icon", - "apple-touch-icon-precomposed", - "apple-touch-icon icon-precomposed" // ComicVine has it combined - }; - - /// - /// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon) - /// - private static readonly Dictionary FaviconUrlMapper = new() - { - ["https://app.plex.tv"] = "https://plex.tv" - }; - /// - /// Cache of the publisher/favicon list - /// - private static readonly TimeSpan CacheDuration = TimeSpan.FromDays(1); - - public CoverDbService(ILogger logger, IDirectoryService directoryService, - IEasyCachingProviderFactory cacheFactory, IHostEnvironment env, IImageService imageService, - IUnitOfWork unitOfWork, IEventHub eventHub) - { - _logger = logger; - _directoryService = directoryService; - _cacheFactory = cacheFactory; - _env = env; - _imageService = imageService; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - } - - /// - /// Downloads the favicon image from a given website URL, optionally falling back to a custom method if standard methods fail. - /// - /// The full URL of the website to extract the favicon from. - /// The desired image encoding format for saving the favicon (e.g., WebP, PNG). - /// - /// A string representing the filename of the downloaded favicon image, saved to the configured favicon directory. - /// - /// - /// Thrown when favicon retrieval fails or if a previously failed domain is detected in cache. - /// - /// - /// This method first checks for a cached failure to avoid re-requesting bad links. - /// It then attempts to parse HTML for `link` tags pointing to `.png` favicons and - /// falls back to an internal fallback method if needed. Valid results are saved to disk. - /// - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) - { - // Parse the URL to get the domain (including subdomain) - var uri = new Uri(url); - var domain = uri.Host.Replace(Environment.NewLine, string.Empty); - var baseUrl = uri.Scheme + "://" + uri.Host; - - - var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); - var res = await provider.GetAsync(baseUrl); - if (res.HasValue) - { - var sanitizedBaseUrl = baseUrl.Sanitize(); - _logger.LogInformation("Kavita has already tried to fetch from {BaseUrl} and failed. Skipping duplicate check", sanitizedBaseUrl); - throw new KavitaException($"Kavita has already tried to fetch from {sanitizedBaseUrl} and failed. Skipping duplicate check"); - } - - await provider.SetAsync(baseUrl, string.Empty, _cacheTime); - if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) - { - url = value; - } - - var correctSizeLink = string.Empty; - - try - { - var htmlContent = url.GetStringAsync().Result; - var htmlDocument = new HtmlDocument(); - htmlDocument.LoadHtml(htmlContent); - - var pngLinks = htmlDocument.DocumentNode.Descendants("link") - .Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty))) - .Select(link => link.GetAttributeValue("href", string.Empty)) - .Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - - correctSizeLink = (pngLinks?.Find(pngLink => pngLink.Contains("32")) ?? pngLinks?.FirstOrDefault()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading favicon.png for {Domain}, will try fallback methods", domain); - } - - try - { - if (string.IsNullOrEmpty(correctSizeLink)) - { - correctSizeLink = await FallbackToKavitaReaderFavicon(baseUrl); - } - if (string.IsNullOrEmpty(correctSizeLink)) - { - throw new KavitaException($"Could not grab favicon from {baseUrl}"); - } - - var finalUrl = correctSizeLink; - - // If starts with //, it's coming usually from an offsite cdn - if (correctSizeLink.StartsWith("//")) - { - finalUrl = "https:" + correctSizeLink; - } - else if (!correctSizeLink.StartsWith(uri.Scheme)) - { - finalUrl = Url.Combine(baseUrl, correctSizeLink); - } - - _logger.LogTrace("Fetching favicon from {Url}", finalUrl); - // Download the favicon.ico file using Flurl - var faviconStream = await finalUrl - .AllowHttpStatus("2xx,304") - .GetStreamAsync(); - - // Create the destination file path - using var image = Image.PngloadStream(faviconStream); - var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); - - image.WriteToFile(Path.Combine(_directoryService.FaviconDirectory, filename)); - _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); - - return filename; - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading favicon for {Domain}", domain); - throw; - } - } - - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat) - { - try - { - // Sanitize user input - publisherName = publisherName.Replace(Environment.NewLine, string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty); - var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Publisher); - var res = await provider.GetAsync(publisherName); - if (res.HasValue) - { - _logger.LogInformation("Kavita has already tried to fetch Publisher: {PublisherName} and failed. Skipping duplicate check", publisherName); - throw new KavitaException($"Kavita has already tried to fetch Publisher: {publisherName} and failed. Skipping duplicate check"); - } - - await provider.SetAsync(publisherName, string.Empty, _cacheTime); - var publisherLink = await FallbackToKavitaReaderPublisher(publisherName); - if (string.IsNullOrEmpty(publisherLink)) - { - throw new KavitaException($"Could not grab publisher image for {publisherName}"); - } - - // Create the destination file path - var filename = ImageService.GetPublisherFormat(publisherName, encodeFormat); - - _logger.LogTrace("Fetching publisher image from {Url}", publisherLink.Sanitize()); - await DownloadImageFromUrl(publisherName, encodeFormat, publisherLink, _directoryService.PublisherDirectory); - - _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName.Sanitize()); - - return filename; - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading image for {PublisherName}", publisherName.Sanitize()); - throw; - } - } - - /// - /// Attempts to download the Person image from CoverDB while matching against metadata within the Person - /// - /// - /// - /// Person image (in correct directory) or null if not found/error - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat) - { - try - { - var personImageLink = await GetCoverPersonImagePath(person); - if (string.IsNullOrEmpty(personImageLink)) - { - throw new KavitaException($"Could not grab person image for {person.Name}"); - } - return await DownloadPersonImageAsync(person, encodeFormat, personImageLink); - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading image for {PersonName}", person.Name); - } - - return null; - } - - /// - /// Attempts to download the Person cover image from a Url - /// - /// - /// - /// - /// - /// - /// - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url) - { - try - { - var personImageLink = await GetCoverPersonImagePath(person); - if (string.IsNullOrEmpty(personImageLink)) - { - throw new KavitaException($"Could not grab person image for {person.Name}"); - } - - - var filename = await DownloadImageFromUrl(ImageService.GetPersonFormat(person.Id), encodeFormat, personImageLink); - - _logger.LogDebug("Person image for {PersonName} downloaded and saved successfully", person.Name); - - return filename; - } catch (Exception ex) - { - _logger.LogError(ex, "Error downloading image for {PersonName}", person.Name); - } - - return null; - } - - private async Task DownloadImageFromUrl(string filenameWithoutExtension, EncodeFormat encodeFormat, string url, string? targetDirectory = null) - { - // TODO: I need to unit test this to ensure it works when overwriting, etc - - // Target Directory defaults to CoverImageDirectory, but can be temp for when comparison between images is used - targetDirectory ??= _directoryService.CoverImageDirectory; - - // Create the destination file path - var filename = filenameWithoutExtension + encodeFormat.GetExtension(); - var targetFile = Path.Combine(targetDirectory, filename); - - _logger.LogTrace("Fetching person image from {Url}", url.Sanitize()); - // Download the file using Flurl - var imageStream = await url - .AllowHttpStatus("2xx,304") - .GetStreamAsync(); - - using var image = Image.NewFromStream(imageStream); - try - { - image.WriteToFile(targetFile); - } - catch (Exception ex) - { - switch (encodeFormat) - { - case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - } - - return filename; - } - - private async Task GetCoverPersonImagePath(Person person) - { - var tempFile = Path.Join(_directoryService.LongTermCacheDirectory, "people.yml"); - - // Check if the file already exists and skip download in Development environment - if (File.Exists(tempFile)) - { - if (_env.IsDevelopment()) - { - _logger.LogInformation("Using existing people.yml file in Development environment"); - } - else - { - // Remove file if not in Development and file is older than 7 days - if (File.GetLastWriteTime(tempFile) < DateTime.Now.AddDays(-7)) - { - File.Delete(tempFile); - } - } - } - - // Download the file if it doesn't exist or was deleted due to age - if (!File.Exists(tempFile)) - { - var masterPeopleFile = await $"{NewHost}people/people.yml" - .DownloadFileAsync(_directoryService.LongTermCacheDirectory); - - if (!File.Exists(tempFile) || string.IsNullOrEmpty(masterPeopleFile)) - { - _logger.LogError("Could not download people.yml from Github"); - return null; - } - } - - - var coverDbRepository = new CoverDbRepository(tempFile); - - var coverAuthor = coverDbRepository.FindBestAuthorMatch(person); - if (coverAuthor == null || string.IsNullOrEmpty(coverAuthor.ImagePath)) - { - throw new KavitaException($"Could not grab person image for {person.Name}"); - } - - return $"{NewHost}{coverAuthor.ImagePath}"; - } - - private async Task FallbackToKavitaReaderFavicon(string baseUrl) - { - const string urlsFileName = "publishers.txt"; - var correctSizeLink = string.Empty; - var allOverrides = await GetCachedData(urlsFileName) ?? - await $"{NewHost}favicons/{urlsFileName}".GetStringAsync(); - - // Cache immediately - await CacheDataAsync(urlsFileName, allOverrides); - - - if (string.IsNullOrEmpty(allOverrides)) return correctSizeLink; - - var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); - var externalFile = allOverrides - .Split("\n") - .FirstOrDefault(url => - cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || - cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) - )); - - if (string.IsNullOrEmpty(externalFile)) - { - throw new KavitaException($"Could not grab favicon from {baseUrl.Sanitize()}"); - } - - return $"{NewHost}favicons/{externalFile}"; - } - - private async Task FallbackToKavitaReaderPublisher(string publisherName) - { - const string publisherFileName = "publishers.txt"; - var allOverrides = await GetCachedData(publisherFileName) ?? - await $"{NewHost}publishers/{publisherFileName}".GetStringAsync(); - - // Cache immediately - await CacheDataAsync(publisherFileName, allOverrides); - - if (string.IsNullOrEmpty(allOverrides)) return string.Empty; - - var externalFile = allOverrides - .Split("\n") - .Select(publisherLine => - { - var tokens = publisherLine.Split("|"); - if (tokens.Length != 2) return null; - var aliases = tokens[0]; - // Multiple publisher aliases are separated by # - if (aliases.Split("#").Any(name => name.ToLowerInvariant().Trim().Equals(publisherName.ToLowerInvariant().Trim()))) - { - return tokens[1]; - } - return null; - }) - .FirstOrDefault(url => !string.IsNullOrEmpty(url)); - - if (string.IsNullOrEmpty(externalFile)) - { - throw new KavitaException($"Could not grab publisher image for {publisherName}"); - } - - return $"{NewHost}publishers/{externalFile}"; - } - - private async Task CacheDataAsync(string fileName, string? content) - { - if (content == null) return; - - try - { - var filePath = _directoryService.FileSystem.Path.Join(_directoryService.LongTermCacheDirectory, fileName); - await File.WriteAllTextAsync(filePath, content); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to cache {FileName}", fileName); - } - } - - - private async Task GetCachedData(string cacheFile) - { - // Form the full file path: - var filePath = _directoryService.FileSystem.Path.Join(_directoryService.LongTermCacheDirectory, cacheFile); - if (!File.Exists(filePath)) return null; - - var fileInfo = new FileInfo(filePath); - if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) - { - return await File.ReadAllTextAsync(filePath); - } - - return null; - } - - /// - /// - /// - /// - /// - /// - /// Will check against all known null image placeholders to avoid writing it - public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false) - { - if (!string.IsNullOrEmpty(url)) - { - var tempDir = _directoryService.TempDirectory; - var format = ImageService.GetPersonFormat(person.Id); - var finalFileName = format + ".webp"; - var tempFileName = format + "_new"; - var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir); - - if (!string.IsNullOrEmpty(tempFilePath)) - { - var tempFullPath = Path.Combine(tempDir, tempFilePath); - var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); - - // Skip setting image if it's similar to a known placeholder - if (checkNoImagePlaceholder) - { - var placeholderPath = Path.Combine(_directoryService.AssetsDirectory, "anilist-no-image-placeholder.jpg"); - var similarity = placeholderPath.CalculateSimilarity(tempFullPath); - if (similarity >= 0.9f) - { - _logger.LogInformation("Skipped setting placeholder image for person {PersonId} due to high similarity ({Similarity})", person.Id, similarity); - _directoryService.DeleteFiles([tempFullPath]); - return; - } - } - - try - { - if (!string.IsNullOrEmpty(person.CoverImage)) - { - var existingPath = Path.Combine(_directoryService.CoverImageDirectory, person.CoverImage); - var betterImage = existingPath.GetBetterImage(tempFullPath)!; - - var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); - if (choseNewImage) - { - _directoryService.DeleteFiles([existingPath]); - _directoryService.CopyFile(tempFullPath, finalFullPath); - person.CoverImage = finalFileName; - } - else - { - _directoryService.DeleteFiles([tempFullPath]); - return; - } - } - else - { - _directoryService.CopyFile(tempFullPath, finalFullPath); - person.CoverImage = finalFileName; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error choosing better image for Person: {PersonId}", person.Id); - _directoryService.CopyFile(tempFullPath, finalFullPath); - person.CoverImage = finalFileName; - } - - _directoryService.DeleteFiles([tempFullPath]); - - person.CoverImageLocked = true; - _imageService.UpdateColorScape(person); - _unitOfWork.PersonRepository.Update(person); - } - } - else - { - person.CoverImage = string.Empty; - person.CoverImageLocked = false; - _imageService.UpdateColorScape(person); - _unitOfWork.PersonRepository.Update(person); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false); - } - } - - /// - /// Sets the series cover by url - /// - /// - /// - /// - /// If images are similar, will choose the higher quality image - public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false) - { - if (!string.IsNullOrEmpty(url)) - { - var tempDir = _directoryService.TempDirectory; - var format = ImageService.GetSeriesFormat(series.Id); - var finalFileName = format + ".webp"; - var tempFileName = format + "_new"; - var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir); - - if (!string.IsNullOrEmpty(tempFilePath)) - { - var tempFullPath = Path.Combine(tempDir, tempFilePath); - var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); - - if (chooseBetterImage && !string.IsNullOrEmpty(series.CoverImage)) - { - try - { - var existingPath = Path.Combine(_directoryService.CoverImageDirectory, series.CoverImage); - var betterImage = existingPath.GetBetterImage(tempFullPath)!; - - var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); - if (choseNewImage) - { - // Don't delete the Series cover unless it is an override, otherwise the first chapter will be null - if (existingPath.Contains(ImageService.GetSeriesFormat(series.Id))) - { - _directoryService.DeleteFiles([existingPath]); - } - - _directoryService.CopyFile(tempFullPath, finalFullPath); - series.CoverImage = finalFileName; - } - else - { - _directoryService.DeleteFiles([tempFullPath]); - return; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error choosing better image for Series: {SeriesId}", series.Id); - _directoryService.CopyFile(tempFullPath, finalFullPath); - series.CoverImage = finalFileName; - } - } - else - { - _directoryService.CopyFile(tempFullPath, finalFullPath); - series.CoverImage = finalFileName; - } - - _directoryService.DeleteFiles([tempFullPath]); - series.CoverImageLocked = true; - _imageService.UpdateColorScape(series); - _unitOfWork.SeriesRepository.Update(series); - } - } - else - { - series.CoverImage = null; - series.CoverImageLocked = false; - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null"); - _imageService.UpdateColorScape(series); - _unitOfWork.SeriesRepository.Update(series); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); - } - } - - // TODO: Refactor this to IHasCoverImage instead of a hard entity type - public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) - { - if (!string.IsNullOrEmpty(url)) - { - var tempDirectory = _directoryService.TempDirectory; - var finalFileName = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + ".webp"; - var tempFileName = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + "_new"; - - var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDirectory); - - if (!string.IsNullOrEmpty(tempFilePath)) - { - var tempFullPath = Path.Combine(tempDirectory, tempFilePath); - var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); - - if (chooseBetterImage && !string.IsNullOrEmpty(chapter.CoverImage)) - { - try - { - var existingPath = Path.Combine(_directoryService.CoverImageDirectory, chapter.CoverImage); - var betterImage = existingPath.GetBetterImage(tempFullPath)!; - var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); - - if (choseNewImage) - { - // This will fail if Cover gen is done just before this as there is a bug with files getting locked. - _directoryService.DeleteFiles([existingPath]); - _directoryService.CopyFile(tempFullPath, finalFullPath); - _directoryService.DeleteFiles([tempFullPath]); - } - else - { - _directoryService.DeleteFiles([tempFullPath]); - return; - } - - chapter.CoverImage = finalFileName; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue trying to choose a better cover image for Chapter: {FileName} ({ChapterId})", chapter.Range, chapter.Id); - } - } - else - { - // No comparison needed, just copy and rename to final - _directoryService.CopyFile(tempFullPath, finalFullPath); - _directoryService.DeleteFiles([tempFullPath]); - chapter.CoverImage = finalFileName; - } - - chapter.CoverImageLocked = true; - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); - } - } - else - { - chapter.CoverImage = null; - chapter.CoverImageLocked = false; - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync( - MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), - false - ); - } - } - - /// - /// - /// - /// - /// Filename without extension - /// - /// Not useable with fromBase64. Allows a different directory to be written to - /// - private async Task CreateThumbnail(string url, string filenameWithoutExtension, bool fromBase64 = true, string? targetDirectory = null) - { - targetDirectory ??= _directoryService.CoverImageDirectory; - - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var encodeFormat = settings.EncodeMediaAs; - var coverImageSize = settings.CoverImageSize; - - if (fromBase64) - { - return _imageService.CreateThumbnailFromBase64(url, - filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width); - } - - return await DownloadImageFromUrl(filenameWithoutExtension, encodeFormat, url, targetDirectory); - } -} diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index bff7001bd..4ebbf57c6 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.Logging; using VersOne.Epub; namespace API.Services.Tasks.Metadata; -#nullable enable public interface IWordCountAnalyzerService { @@ -33,19 +32,17 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService private readonly IEventHub _eventHub; private readonly ICacheHelper _cacheHelper; private readonly IReaderService _readerService; - private readonly IMediaErrorService _mediaErrorService; private const int AverageCharactersPerWord = 5; public WordCountAnalyzerService(ILogger logger, IUnitOfWork unitOfWork, IEventHub eventHub, - ICacheHelper cacheHelper, IReaderService readerService, IMediaErrorService mediaErrorService) + ICacheHelper cacheHelper, IReaderService readerService) { _logger = logger; _unitOfWork = unitOfWork; _eventHub = eventHub; _cacheHelper = cacheHelper; _readerService = readerService; - _mediaErrorService = mediaErrorService; } @@ -160,14 +157,10 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService { // This compares if it's changed since a file scan only var firstFile = chapter.Files.FirstOrDefault(); - if (firstFile == null || !_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, - forceUpdate, + if (firstFile == null) return; + if (!_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate, firstFile)) - { - volume.WordCount += chapter.WordCount; - series.WordCount += chapter.WordCount; continue; - } if (series.Format == MangaFormat.Epub) { @@ -179,7 +172,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var pageCounter = 1; try { - using var book = await EpubReader.OpenBookAsync(filePath, BookService.LenientBookReaderOptions); + using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions); var totalPages = book.Content.Html.Local; foreach (var bookPage in totalPages) @@ -190,7 +183,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress, ProgressEventType.Updated, useFileName ? filePath : series.Name)); - sum += await GetWordCountFromHtml(bookPage, filePath); + sum += await GetWordCountFromHtml(bookPage); pageCounter++; } @@ -217,7 +210,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService chapter.MinHoursToRead = est.MinHours; chapter.MaxHoursToRead = est.MaxHours; chapter.AvgHoursToRead = est.AvgHours; - foreach (var file in chapter.Files) { UpdateFileAnalysis(file); @@ -248,23 +240,13 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } - private async Task GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath) + private static async Task GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile) { - try - { - var doc = new HtmlDocument(); - doc.LoadHtml(await bookFile.ReadContentAsync()); + var doc = new HtmlDocument(); + doc.LoadHtml(await bookFile.ReadContentAsync()); - var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]"); - return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0; - } - catch (EpubContentException ex) - { - _logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath); - await _mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService, - $"Invalid Epub Metadata, {bookFile.FilePath} does not exist", ex.Message); - return 0; - } + var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]"); + return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0; } } diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index fec0304a8..6e844fbe3 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; -#nullable enable public interface ILibraryWatcher { @@ -56,9 +55,9 @@ public class LibraryWatcher : ILibraryWatcher /// /// Counts within a time frame how many times the buffer became full. Is used to reschedule LibraryWatcher to start monitoring much later rather than instantly /// - private static int _bufferFullCounter; - private static int _restartCounter; - private static DateTime _lastErrorTime = DateTime.MinValue; + private int _bufferFullCounter; + private int _restartCounter; + private DateTime _lastErrorTime = DateTime.MinValue; /// /// Used to lock buffer Full Counter /// @@ -148,30 +147,15 @@ public class LibraryWatcher : ILibraryWatcher private void OnChanged(object sender, FileSystemEventArgs e) { - _logger.LogTrace("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType); + _logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}, {ChangeType}", e.FullPath, e.Name, e.ChangeType); if (e.ChangeType != WatcherChangeTypes.Changed) return; - - var isDirectoryChange = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)); - - if (TaskScheduler.HasAlreadyEnqueuedTask("LibraryWatcher", "ProcessChange", [e.FullPath, isDirectoryChange], - checkRunningJobs: true)) - { - return; - } - - BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, isDirectoryChange)); + BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)))); } private void OnCreated(object sender, FileSystemEventArgs e) { - _logger.LogTrace("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name); - var isDirectoryChange = !_directoryService.FileSystem.File.Exists(e.Name); - if (TaskScheduler.HasAlreadyEnqueuedTask("LibraryWatcher", "ProcessChange", [e.FullPath, isDirectoryChange], - checkRunningJobs: true)) - { - return; - } - BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, isDirectoryChange)); + _logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name); + BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name))); } /// @@ -182,12 +166,7 @@ public class LibraryWatcher : ILibraryWatcher private void OnDeleted(object sender, FileSystemEventArgs e) { var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)); if (!isDirectory) return; - _logger.LogTrace("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name); - if (TaskScheduler.HasAlreadyEnqueuedTask("LibraryWatcher", "ProcessChange", [e.FullPath, true], - checkRunningJobs: true)) - { - return; - } + _logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name); BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true)); } @@ -199,7 +178,7 @@ public class LibraryWatcher : ILibraryWatcher /// private void OnError(object sender, ErrorEventArgs e) { - _logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers {Current}/{Total}", _bufferFullCounter, 3); + _logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers"); bool condition; lock (Lock) { @@ -278,23 +257,21 @@ public class LibraryWatcher : ILibraryWatcher _logger.LogTrace("Folder path: {FolderPath}", fullPath); if (string.IsNullOrEmpty(fullPath)) { - _logger.LogInformation("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath); + _logger.LogTrace("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath); return; } - _taskScheduler.ScanFolder(fullPath, filePath, _queueWaitTime); + _taskScheduler.ScanFolder(fullPath, _queueWaitTime); } catch (Exception ex) { _logger.LogError(ex, "[LibraryWatcher] An error occured when processing a watch event"); } - _logger.LogTrace("[LibraryWatcher] ProcessChange completed in {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); + _logger.LogDebug("[LibraryWatcher] ProcessChange completed in {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); } private string GetFolder(string filePath, IEnumerable libraryFolders) { - // TODO: I can optimize this to avoid a library scan and instead do a Series Scan by finding the series that has a lowestFolderPath higher or equal to the filePath - var parentDirectory = _directoryService.GetParentDirectoryName(filePath); _logger.LogTrace("[LibraryWatcher] Parent Directory: {ParentDirectory}", parentDirectory); if (string.IsNullOrEmpty(parentDirectory)) return string.Empty; @@ -307,10 +284,10 @@ public class LibraryWatcher : ILibraryWatcher var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList(); _logger.LogTrace("[LibraryWatcher] Root Folders: {RootFolders}", rootFolder); - if (rootFolder.Count == 0) return string.Empty; + if (!rootFolder.Any()) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); + return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1])); } @@ -318,7 +295,7 @@ public class LibraryWatcher : ILibraryWatcher /// This is called via Hangfire to decrement the counter. Must work around a lock /// // ReSharper disable once MemberCanBePrivate.Global - public static void UpdateLastBufferOverflow() + public void UpdateLastBufferOverflow() { lock (Lock) { diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 83558eaa0..f898d77cc 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -1,22 +1,17 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Services.Tasks.Scanner.Parser; using API.SignalR; -using ExCSS; using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; -#nullable enable public class ParsedSeries { @@ -32,59 +27,11 @@ public class ParsedSeries /// Format of the Series /// public required MangaFormat Format { get; init; } - /// - /// Has this Series changed or not aka do we need to process it or not. - /// - public bool HasChanged { get; set; } -} - -public class ScanResult -{ - /// - /// A list of files in the Folder. Empty if HasChanged = false - /// - public IList Files { get; set; } - /// - /// A nested folder from Library Root (at any level) - /// - public string Folder { get; set; } - /// - /// The library root - /// - public string LibraryRoot { get; set; } - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and Files empty - /// - public bool HasChanged { get; set; } - /// - /// Set in Stage 2: Parsed Info from the Files - /// - public IList ParserInfos { get; set; } -} - -/// -/// The final product of ParseScannedFiles. This has all the processed parserInfo and is ready for tracking/processing into entities -/// -public class ScannedSeriesResult -{ - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and indicates that upstream should count this as skipped - /// - public bool HasChanged { get; set; } - /// - /// The Parsed Series information used for tracking - /// - public ParsedSeries ParsedSeries { get; set; } - /// - /// Parsed files - /// - public IList ParsedInfos { get; set; } } public class SeriesModified { - public required string? FolderPath { get; set; } - public required string? LowestFolderPath { get; set; } + public required string FolderPath { get; set; } public required string SeriesName { get; set; } public DateTime LastScanned { get; set; } public MangaFormat Format { get; set; } @@ -119,282 +66,102 @@ public class ParseScannedFiles _eventHub = eventHub; } + /// /// This will Scan all files in a folder path. For each folder within the folderPath, FolderAction will be invoked for all files contained /// /// Scan directory by directory and for each, call folderAction /// A dictionary mapping a normalized path to a list of to help scanner skip I/O /// A library folder or series folder + /// A callback async Task to be called once all files for each folder path are found /// If we should bypass any folder last write time checks on the scan and force I/O - public async Task> ScanFiles(string folderPath, bool scanDirectoryByDirectory, - IDictionary> seriesPaths, Library library, bool forceCheck = false) + public async Task ProcessFiles(string folderPath, bool scanDirectoryByDirectory, + IDictionary> seriesPaths, Func, string,Task> folderAction, bool forceCheck = false) { - var fileExtensions = string.Join("|", library.LibraryFileTypes.Select(l => l.FileTypeGroup.GetRegex())); - - // If there are no library file types, skip scanning entirely - if (string.IsNullOrWhiteSpace(fileExtensions)) - { - return ArraySegment.Empty; - } - - var matcher = BuildMatcher(library); - - var result = new List(); - - // Not to self: this whole thing can be parallelized because we don't deal with any DB or global state + string normalizedPath; if (scanDirectoryByDirectory) { - return await ScanDirectories(folderPath, seriesPaths, library, forceCheck, matcher, result, fileExtensions); - } + // This is used in library scan, so we should check first for a ignore file and use that here as well + var potentialIgnoreFile = _directoryService.FileSystem.Path.Join(folderPath, DirectoryService.KavitaIgnoreFile); + var matcher = _directoryService.CreateMatcherFromFile(potentialIgnoreFile); + var directories = _directoryService.GetDirectories(folderPath, matcher).ToList(); - return await ScanSingleDirectory(folderPath, seriesPaths, library, forceCheck, result, fileExtensions, matcher); - } - - private async Task> ScanDirectories(string folderPath, IDictionary> seriesPaths, - Library library, bool forceCheck, GlobMatcher matcher, List result, string fileExtensions) - { - var allDirectories = _directoryService.GetAllDirectories(folderPath, matcher) - .Select(Parser.Parser.NormalizePath) - .OrderByDescending(d => d.Length) - .ToList(); - - var processedDirs = new HashSet(); - - _logger.LogDebug("[ScannerService] Step 1.C Found {DirectoryCount} directories to process for {FolderPath}", allDirectories.Count, folderPath); - foreach (var directory in allDirectories) - { - // Don't process any folders where we've already scanned everything below - if (processedDirs.Any(d => d.StartsWith(directory + Path.AltDirectorySeparatorChar) || d.Equals(directory))) + foreach (var directory in directories) { - var hasChanged = !HasSeriesFolderNotChangedSinceLastScan(library, seriesPaths, directory, forceCheck); - // Skip this directory as we've already processed a parent unless there are loose files at that directory - // and they have changes - CheckSurfaceFiles(result, directory, folderPath, fileExtensions, matcher, hasChanged); - continue; + normalizedPath = Parser.Parser.NormalizePath(directory); + if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedPath, forceCheck)) + { + await folderAction(new List(), directory); + } + else + { + // For a scan, this is doing everything in the directory loop before the folder Action is called...which leads to no progress indication + await folderAction(_directoryService.ScanFiles(directory, matcher), directory); + } } - // Skip directories ending with "Specials", let the parent handle it - if (directory.EndsWith("Specials", StringComparison.OrdinalIgnoreCase)) - { - // Log or handle that we are skipping this directory - _logger.LogDebug("Skipping {Directory} as it ends with 'Specials'", directory); - continue; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent(directory, library.Name, ProgressEventType.Updated)); - - if (HasSeriesFolderNotChangedSinceLastScan(library, seriesPaths, directory, forceCheck)) - { - HandleUnchangedFolder(result, folderPath, directory); - } - else - { - PerformFullScan(result, directory, folderPath, fileExtensions, matcher); - } - - processedDirs.Add(directory); - } - - return result; - } - - /// - /// Checks against all folder paths on file if the last scanned is >= the directory's last write time, down to the second - /// - /// - /// - /// This should be normalized - /// - /// - private bool HasSeriesFolderNotChangedSinceLastScan(Library library, IDictionary> seriesPaths, string directory, bool forceCheck) - { - // Reverting code from: https://github.com/Kareadita/Kavita/pull/3619/files#diff-0625df477047ab9d8e97a900201f2f29b2dc0599ba58eb75cfbbd073a9f3c72f - // This is to be able to release hotfix and tackle this in appropriate time - - // With the bottom-up approach, this can report a false positive where a nested folder will get scanned even though a parent is the series - // This can't really be avoided. This is more likely to happen on Image chapter folder library layouts. - if (forceCheck || !seriesPaths.TryGetValue(directory, out var seriesList)) - { - return false; - } - - // if (forceCheck) - // { - // return false; - // } - - // TryGetSeriesList falls back to parent folders to match to seriesList - // var seriesList = TryGetSeriesList(library, seriesPaths, directory); - // if (seriesList == null) - // { - // return false; - // } - - foreach (var series in seriesList) - { - var lastWriteTime = _directoryService.GetLastWriteTime(series.LowestFolderPath!).Truncate(TimeSpan.TicksPerSecond); - var seriesLastScanned = series.LastScanned.Truncate(TimeSpan.TicksPerSecond); - if (seriesLastScanned < lastWriteTime) - { - return false; - } - } - - return true; - } - - private IList? TryGetSeriesList(Library library, IDictionary> seriesPaths, string directory) - { - if (seriesPaths.Count == 0) - { - return null; - } - - if (string.IsNullOrEmpty(directory)) - { - return null; - } - - if (library.Folders.Any(fp => fp.Path.Equals(directory))) - { - return null; - } - - if (seriesPaths.TryGetValue(directory, out var seriesList)) - { - return seriesList; - } - - return TryGetSeriesList(library, seriesPaths, _directoryService.GetParentDirectoryName(directory)); - } - - /// - /// Handles directories that haven't changed since the last scan. - /// - private void HandleUnchangedFolder(List result, string folderPath, string directory) - { - if (result.Exists(r => r.Folder == directory)) - { - _logger.LogDebug("[ProcessFiles] Skipping adding {Directory} as it's already added, this indicates a bad layout issue", directory); - } - else - { - _logger.LogDebug("[ProcessFiles] Skipping {Directory} as it hasn't changed since last scan", directory); - result.Add(CreateScanResult(directory, folderPath, false, ArraySegment.Empty)); - } - } - - /// - /// Performs a full scan of the directory and adds it to the result. - /// - private void PerformFullScan(List result, string directory, string folderPath, string fileExtensions, GlobMatcher matcher) - { - _logger.LogDebug("[ProcessFiles] Performing full scan on {Directory}", directory); - var files = _directoryService.ScanFiles(directory, fileExtensions, matcher); - if (files.Count == 0) - { - _logger.LogDebug("[ProcessFiles] Empty directory: {Directory}. Keeping empty will cause Kavita to scan this each time", directory); - } - result.Add(CreateScanResult(directory, folderPath, true, files)); - } - - /// - /// Performs a full scan of the directory and adds it to the result. - /// - private void CheckSurfaceFiles(List result, string directory, string folderPath, string fileExtensions, GlobMatcher matcher, bool hasChanged) - { - var files = _directoryService.ScanFiles(directory, fileExtensions, matcher, SearchOption.TopDirectoryOnly); - if (files.Count == 0) - { return; } - // Revert of https://github.com/Kareadita/Kavita/pull/3629/files#diff-0625df477047ab9d8e97a900201f2f29b2dc0599ba58eb75cfbbd073a9f3c72f - // for Hotfix v0.8.5.x - result.Add(CreateScanResult(directory, folderPath, true, files)); + + normalizedPath = Parser.Parser.NormalizePath(folderPath); + if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedPath, forceCheck)) + { + await folderAction(new List(), folderPath); + return; + } + // We need to calculate all folders till library root and see if any kavitaignores + var seriesMatcher = BuildIgnoreFromLibraryRoot(folderPath, seriesPaths); + + await folderAction(_directoryService.ScanFiles(folderPath, seriesMatcher), folderPath); } /// - /// Scans a single directory and processes the scan result. + /// Used in ScanSeries, which enters at a lower level folder and hence needs a .kavitaignore from higher (up to root) to be built before + /// the scan takes place. /// - private async Task> ScanSingleDirectory(string folderPath, IDictionary> seriesPaths, Library library, bool forceCheck, List result, - string fileExtensions, GlobMatcher matcher) + /// + /// + /// A GlobMatter. Empty if not applicable + private GlobMatcher BuildIgnoreFromLibraryRoot(string folderPath, IDictionary> seriesPaths) { - var normalizedPath = Parser.Parser.NormalizePath(folderPath); - var libraryRoot = - library.Folders.FirstOrDefault(f => - normalizedPath.Contains(Parser.Parser.NormalizePath(f.Path)))?.Path ?? - folderPath; - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent(normalizedPath, library.Name, ProgressEventType.Updated)); - - if (HasSeriesFolderNotChangedSinceLastScan(library, seriesPaths, normalizedPath, forceCheck)) + var seriesMatcher = new GlobMatcher(); + try { - result.Add(CreateScanResult(folderPath, libraryRoot, false, ArraySegment.Empty)); - } - else - { - result.Add(CreateScanResult(folderPath, libraryRoot, true, - _directoryService.ScanFiles(folderPath, fileExtensions, matcher))); - } + var roots = seriesPaths[folderPath][0].LibraryRoots.Select(Parser.Parser.NormalizePath).ToList(); + var libraryFolder = roots.SingleOrDefault(folderPath.Contains); - return result; - } - - private static GlobMatcher BuildMatcher(Library library) - { - var matcher = new GlobMatcher(); - foreach (var pattern in library.LibraryExcludePatterns.Where(p => !string.IsNullOrEmpty(p.Pattern))) - { - matcher.AddExclude(pattern.Pattern); - } - - return matcher; - } - - private static ScanResult CreateScanResult(string folderPath, string libraryRoot, bool hasChanged, - IList files) - { - return new ScanResult() - { - Files = files, - Folder = Parser.Parser.NormalizePath(folderPath), - LibraryRoot = libraryRoot, - HasChanged = hasChanged - }; - } - - /// - /// Processes scanResults to track all series across the combined results. - /// Ensures series are correctly grouped even if they span multiple folders. - /// - /// A collection of scan results - /// A concurrent dictionary to store the tracked series - private void TrackSeriesAcrossScanResults(IList scanResults, ConcurrentDictionary> scannedSeries) - { - // Flatten all ParserInfos from scanResults - var allInfos = scanResults.SelectMany(sr => sr.ParserInfos).ToList(); - - // Iterate through each ParserInfo and track the series - foreach (var info in allInfos) - { - if (info == null) continue; - - try + if (string.IsNullOrEmpty(libraryFolder) || !Directory.Exists(folderPath)) { - TrackSeries(scannedSeries, info); + return seriesMatcher; } - catch (Exception ex) + + var allParents = _directoryService.GetFoldersTillRoot(libraryFolder, folderPath); + var path = libraryFolder; + + // Apply the library root level kavitaignore + var potentialIgnoreFile = _directoryService.FileSystem.Path.Join(path, DirectoryService.KavitaIgnoreFile); + seriesMatcher.Merge(_directoryService.CreateMatcherFromFile(potentialIgnoreFile)); + + // Then apply kavitaignores for each folder down to where the series folder is + foreach (var folderPart in allParents.Reverse()) { - _logger.LogError(ex, "[ScannerService] Exception occurred during tracking {FilePath}. Skipping this file", info?.FullFilePath); + path = Parser.Parser.NormalizePath(Path.Join(libraryFolder, folderPart)); + potentialIgnoreFile = _directoryService.FileSystem.Path.Join(path, DirectoryService.KavitaIgnoreFile); + seriesMatcher.Merge(_directoryService.CreateMatcherFromFile(potentialIgnoreFile)); } } + catch (Exception ex) + { + _logger.LogError(ex, + "[ScannerService] There was an error trying to find and apply .kavitaignores above the Series Folder. Scanning without them present"); + } + + return seriesMatcher; } /// - /// Attempts to either add a new instance of a series mapping to the _scannedSeries bag or adds to an existing. + /// Attempts to either add a new instance of a show mapping to the _scannedSeries bag or adds to an existing. /// This will check if the name matches an existing series name (multiple fields) /// /// A localized list of a series' parsed infos @@ -406,8 +173,6 @@ public class ParseScannedFiles // Check if normalized info.Series already exists and if so, update info to use that name instead info.Series = MergeName(scannedSeries, info); - // BUG: This will fail for Solo Leveling & Solo Leveling (Manga) - var normalizedSeries = info.Series.ToNormalized(); var normalizedSortSeries = info.SeriesSort.ToNormalized(); var normalizedLocalizedSeries = info.LocalizedSeries.ToNormalized(); @@ -425,7 +190,7 @@ public class ParseScannedFiles NormalizedName = normalizedSeries }; - scannedSeries.AddOrUpdate(existingKey, [info], (_, oldValue) => + scannedSeries.AddOrUpdate(existingKey, new List() {info}, (_, oldValue) => { oldValue ??= new List(); if (!oldValue.Contains(info)) @@ -438,13 +203,13 @@ public class ParseScannedFiles } catch (Exception ex) { - _logger.LogCritical("[ScannerService] {SeriesName} matches against multiple series in the parsed series. This indicates a critical kavita issue. Key will be skipped", info.Series); + _logger.LogCritical(ex, "[ScannerService] {SeriesName} matches against multiple series in the parsed series. This indicates a critical kavita issue. Key will be skipped", info.Series); foreach (var seriesKey in scannedSeries.Keys.Where(ps => ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries) || ps.NormalizedName.Equals(normalizedLocalizedSeries) || ps.NormalizedName.Equals(normalizedSortSeries)))) { - _logger.LogCritical("[ScannerService] Matches: '{SeriesName}' matches on '{SeriesKey}'", info.Series, seriesKey.Name); + _logger.LogCritical("[ScannerService] Matches: {SeriesName} matches on {SeriesKey}", info.Series, seriesKey.Name); } } } @@ -483,12 +248,11 @@ public class ParseScannedFiles } catch (Exception ex) { - _logger.LogCritical("[ScannerService] Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath); + _logger.LogCritical(ex, "[ScannerService] Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath); var values = scannedSeries.Where(p => (p.Key.NormalizedName.ToNormalized() == normalizedSeries || p.Key.NormalizedName.ToNormalized() == normalizedLocalSeries) && p.Key.Format == info.Format); - foreach (var pair in values) { _logger.LogCritical("[ScannerService] Duplicate Series in DB matches with {SeriesName}: {DuplicateName}", info.Series, pair.Key.Name); @@ -499,142 +263,117 @@ public class ParseScannedFiles return info.Series; } + /// /// This will process series by folder groups. This is used solely by ScanSeries /// - /// This should have the FileTypes included + /// /// + /// /// If true, does a directory scan first (resulting in folders being tackled in parallel), else does an immediate scan files /// A map of Series names -> existing folder paths to handle skipping folders + /// Action which returns if the folder was skipped and the infos from said folder /// Defaults to false /// - public async Task> ScanLibrariesForSeries(Library library, - IList folders, bool isLibraryScan, - IDictionary> seriesPaths, bool forceCheck = false) + public async Task ScanLibrariesForSeries(LibraryType libraryType, + IEnumerable folders, string libraryName, bool isLibraryScan, + IDictionary> seriesPaths, Func>, Task>? processSeriesInfos, bool forceCheck = false) { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent("File Scan Starting", library.Name, ProgressEventType.Started)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started)); - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1.A: Process {FolderCount} folders", library.Name, folders.Count); - var processedScannedSeries = new ConcurrentBag(); - - foreach (var folder in folders) + foreach (var folderPath in folders) { try { - await ScanAndParseFolder(folder, library, isLibraryScan, seriesPaths, processedScannedSeries, forceCheck); + await ProcessFiles(folderPath, isLibraryScan, seriesPaths, ProcessFolder, forceCheck); } catch (ArgumentException ex) { - _logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folder); + _logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folderPath); } } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent("File Scan Done", library.Name, ProgressEventType.Ended)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended)); + return; - return processedScannedSeries.ToList(); + async Task ProcessFolder(IList files, string folder) + { + var normalizedFolder = Parser.Parser.NormalizePath(folder); + if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedFolder, forceCheck)) + { + var parsedInfos = seriesPaths[normalizedFolder].Select(fp => new ParserInfo() + { + Series = fp.SeriesName, + Format = fp.Format, + }).ToList(); + if (processSeriesInfos != null) + await processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); + _logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed since last scan", folder); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.FileScanProgressEvent("Skipped " + normalizedFolder, libraryName, ProgressEventType.Updated)); + return; + } + + _logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", libraryName, ProgressEventType.Updated)); + if (files.Count == 0) + { + _logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder); + return; + } + + var scannedSeries = new ConcurrentDictionary>(); + var infos = files + .Select(file => _readingItemService.ParseFile(file, folder, libraryType)) + .Where(info => info != null) + .ToList(); + + + MergeLocalizedSeriesWithSeries(infos); + + foreach (var info in infos) + { + try + { + TrackSeries(scannedSeries, info); + } + catch (Exception ex) + { + _logger.LogError(ex, + "[ScannerService] There was an exception that occurred during tracking {FilePath}. Skipping this file", + info?.FullFilePath); + } + } + + foreach (var series in scannedSeries.Keys) + { + if (scannedSeries[series].Count > 0 && processSeriesInfos != null) + { + await processSeriesInfos.Invoke(new Tuple>(false, scannedSeries[series])); + } + } + } } /// - /// Helper method to scan and parse a folder + /// Checks against all folder paths on file if the last scanned is >= the directory's last write down to the second /// - /// - /// - /// /// - /// + /// /// - private async Task ScanAndParseFolder(string folderPath, Library library, - bool isLibraryScan, IDictionary> seriesPaths, - ConcurrentBag processedScannedSeries, bool forceCheck) + /// + private bool HasSeriesFolderNotChangedSinceLastScan(IDictionary> seriesPaths, string normalizedFolder, bool forceCheck = false) { - _logger.LogDebug("\t[ScannerService] Library {LibraryName} Step 1.B: Scan files in {Folder}", library.Name, folderPath); - var scanResults = await ScanFiles(folderPath, isLibraryScan, seriesPaths, library, forceCheck); + if (forceCheck) return false; - // Aggregate the scanned series across all scanResults - var scannedSeries = new ConcurrentDictionary>(); - - _logger.LogDebug("\t[ScannerService] Library {LibraryName} Step 1.C: Process files in {Folder}", library.Name, folderPath); - foreach (var scanResult in scanResults) - { - await ParseFiles(scanResult, seriesPaths, library); - } - - _logger.LogDebug("\t[ScannerService] Library {LibraryName} Step 1.D: Merge any localized series with series {Folder}", library.Name, folderPath); - scanResults = MergeLocalizedSeriesAcrossScanResults(scanResults); - - _logger.LogDebug("\t[ScannerService] Library {LibraryName} Step 1.E: Group all parsed data into logical Series", library.Name); - TrackSeriesAcrossScanResults(scanResults, scannedSeries); - - - // Now transform and add to processedScannedSeries AFTER everything is processed - _logger.LogDebug("\t[ScannerService] Library {LibraryName} Step 1.F: Generate Sort Order for Series and Finalize", library.Name); - GenerateProcessedScannedSeries(scannedSeries, scanResults, processedScannedSeries); + return seriesPaths.ContainsKey(normalizedFolder) && seriesPaths[normalizedFolder].All(f => f.LastScanned.Truncate(TimeSpan.TicksPerSecond) >= + _directoryService.GetLastWriteTime(normalizedFolder).Truncate(TimeSpan.TicksPerSecond)); } /// - /// Processes and generates the final results for processedScannedSeries after updating sort order. - /// - /// A concurrent dictionary of tracked series and their parsed infos - /// List of all scan results, used to determine if any series has changed - /// A thread-safe concurrent bag of processed series results - private void GenerateProcessedScannedSeries(ConcurrentDictionary> scannedSeries, IList scanResults, ConcurrentBag processedScannedSeries) - { - // First, update the sort order for all series - UpdateSeriesSortOrder(scannedSeries); - - // Now, generate the final processed scanned series results - CreateFinalSeriesResults(scannedSeries, scanResults, processedScannedSeries); - } - - /// - /// Updates the sort order for all series in the scannedSeries dictionary. - /// - /// A concurrent dictionary of tracked series and their parsed infos - private void UpdateSeriesSortOrder(ConcurrentDictionary> scannedSeries) - { - foreach (var series in scannedSeries.Keys) - { - if (scannedSeries[series].Count <= 0) continue; - - try - { - UpdateSortOrder(scannedSeries, series); // Call to method that updates sort order - } - catch (Exception ex) - { - _logger.LogError(ex, "[ScannerService] Issue occurred while setting IssueOrder for series {SeriesName}", series.Name); - } - } - } - - /// - /// Generates the final processed scanned series results after processing the sort order. - /// - /// A concurrent dictionary of tracked series and their parsed infos - /// List of all scan results, used to determine if any series has changed - /// The list where processed results will be added - private static void CreateFinalSeriesResults(ConcurrentDictionary> scannedSeries, - IList scanResults, ConcurrentBag processedScannedSeries) - { - foreach (var series in scannedSeries.Keys) - { - if (scannedSeries[series].Count <= 0) continue; - - processedScannedSeries.Add(new ScannedSeriesResult - { - HasChanged = scanResults.Any(sr => sr.HasChanged), // Combine HasChanged flag across all scanResults - ParsedSeries = series, - ParsedInfos = scannedSeries[series] - }); - } - } - - /// - /// Merges localized series with the series field across all scan results. - /// Combines ParserInfos from all scanResults and processes them collectively - /// to ensure consistent series names. + /// Checks if there are any ParserInfos that have a Series that matches the LocalizedSeries field in any other info. If so, + /// rewrites the infos with series name instead of the localized name, so they stack. /// /// /// Accel World v01.cbz has Series "Accel World" and Localized Series "World of Acceleration" @@ -642,263 +381,47 @@ public class ParseScannedFiles /// After running this code, we'd have: /// World of Acceleration v02.cbz having Series "Accel World" and Localized Series of "World of Acceleration" /// - /// A collection of scan results - /// A new list of scan results with merged series - private IList MergeLocalizedSeriesAcrossScanResults(IList scanResults) + /// A collection of ParserInfos + private void MergeLocalizedSeriesWithSeries(IReadOnlyCollection infos) { - // Flatten all ParserInfos across scanResults - var allInfos = scanResults.SelectMany(sr => sr.ParserInfos).ToList(); + var hasLocalizedSeries = infos.Any(i => !string.IsNullOrEmpty(i.LocalizedSeries)); + if (!hasLocalizedSeries) return; - // Filter relevant infos (non-special and with localized series) - var relevantInfos = GetRelevantInfos(allInfos); - - if (relevantInfos.Count == 0) return scanResults; - - // Get distinct localized series and process each one - var distinctLocalizedSeries = relevantInfos + var localizedSeries = infos + .Where(i => !i.IsSpecial) .Select(i => i.LocalizedSeries) .Distinct() - .ToList(); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + if (string.IsNullOrEmpty(localizedSeries)) return; - foreach (var localizedSeries in distinctLocalizedSeries) + // NOTE: If we have multiple series in a folder with a localized title, then this will fail. It will group into one series. User needs to fix this themselves. + string? nonLocalizedSeries; + // Normalize this as many of the cases is a capitalization difference + var nonLocalizedSeriesFound = infos + .Where(i => !i.IsSpecial) + .Select(i => i.Series).DistinctBy(Parser.Parser.Normalize).ToList(); + if (nonLocalizedSeriesFound.Count == 1) { - if (string.IsNullOrEmpty(localizedSeries)) continue; - - // Process the localized series for merging - ProcessLocalizedSeries(scanResults, allInfos, relevantInfos, localizedSeries); - } - - // Remove or clear any scan results that now have no ParserInfos after merging - return scanResults.Where(sr => sr.ParserInfos.Count > 0).ToList(); - } - - private static List GetRelevantInfos(List allInfos) - { - return allInfos - .Where(i => !i.IsSpecial && !string.IsNullOrEmpty(i.LocalizedSeries)) - .GroupBy(i => i.Format) - .SelectMany(g => g.ToList()) - .ToList(); - } - - private void ProcessLocalizedSeries(IList scanResults, List allInfos, List relevantInfos, string localizedSeries) - { - var seriesForLocalized = GetSeriesForLocalized(relevantInfos, localizedSeries); - if (seriesForLocalized.Count == 0) return; - - var nonLocalizedSeries = GetNonLocalizedSeries(seriesForLocalized, localizedSeries); - if (nonLocalizedSeries == null) return; - - // Remap and update relevant ParserInfos - RemapSeries(scanResults, allInfos, localizedSeries, nonLocalizedSeries); - - } - - private static List GetSeriesForLocalized(List relevantInfos, string localizedSeries) - { - return relevantInfos - .Where(i => i.LocalizedSeries == localizedSeries) - .DistinctBy(r => r.Series) - .Select(r => r.Series) - .ToList(); - } - - private string? GetNonLocalizedSeries(List seriesForLocalized, string localizedSeries) - { - switch (seriesForLocalized.Count) - { - case 1: - return seriesForLocalized[0]; - case <= 2: - return seriesForLocalized.FirstOrDefault(s => !s.Equals(Parser.Parser.Normalize(localizedSeries))); - default: - _logger.LogError( - "[ScannerService] Multiple series detected across scan results that contain localized series. " + - "This will cause them to group incorrectly. Please separate series into their own dedicated folder: {LocalizedSeries}", - string.Join(", ", seriesForLocalized) - ); - return null; - } - } - - private static void RemapSeries(IList scanResults, List allInfos, string localizedSeries, string nonLocalizedSeries) - { - // If the series names are identical, no remapping is needed (rare but valid) - if (localizedSeries.ToNormalized().Equals(nonLocalizedSeries.ToNormalized())) - { - return; - } - - // Find all infos that need to be remapped from the localized series to the non-localized series - var normalizedLocalizedSeries = localizedSeries.ToNormalized(); - var seriesToBeRemapped = allInfos.Where(i => i.Series.ToNormalized().Equals(normalizedLocalizedSeries)).ToList(); - - foreach (var infoNeedingMapping in seriesToBeRemapped) - { - infoNeedingMapping.Series = nonLocalizedSeries; - - // Find the scan result containing the localized info - var localizedScanResult = scanResults.FirstOrDefault(sr => sr.ParserInfos.Contains(infoNeedingMapping)); - if (localizedScanResult == null) continue; - - // Remove the localized series from this scan result - localizedScanResult.ParserInfos.Remove(infoNeedingMapping); - - // Find the scan result that should be merged with - var nonLocalizedScanResult = scanResults.FirstOrDefault(sr => sr.ParserInfos.Any(pi => pi.Series == nonLocalizedSeries)); - - if (nonLocalizedScanResult == null) continue; - - // Add the remapped info to the non-localized scan result - nonLocalizedScanResult.ParserInfos.Add(infoNeedingMapping); - - // Assign the higher folder path (i.e., the one closer to the root) - //nonLocalizedScanResult.Folder = DirectoryService.GetDeepestCommonPath(localizedScanResult.Folder, nonLocalizedScanResult.Folder); - } - } - - /// - /// For a given ScanResult, sets the ParserInfos on the result - /// - /// - /// - /// - private async Task ParseFiles(ScanResult result, IDictionary> seriesPaths, Library library) - { - var normalizedFolder = Parser.Parser.NormalizePath(result.Folder); - - // If folder hasn't changed, generate fake ParserInfos - if (!result.HasChanged) - { - result.ParserInfos = seriesPaths[normalizedFolder] - .Select(fp => new ParserInfo { Series = fp.SeriesName, Format = fp.Format }) - .ToList(); - - // // We are certain TryGetSeriesList will return a valid result here, if the series wasn't present yet. It will have been changed. - // result.ParserInfos = TryGetSeriesList(library, seriesPaths, normalizedFolder)! - // .Select(fp => new ParserInfo { Series = fp.SeriesName, Format = fp.Format }) - // .ToList(); - - _logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed", normalizedFolder); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent($"Skipped {normalizedFolder}", library.Name, ProgressEventType.Updated)); - return; - } - - var files = result.Files; - var fileCount = files.Count; - - if (fileCount == 0) - { - _logger.LogInformation("[ScannerService] {Folder} is empty or has no matching file types", normalizedFolder); - result.ParserInfos = ArraySegment.Empty; - return; - } - - _logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, normalizedFolder); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent($"{fileCount} files in {normalizedFolder}", library.Name, ProgressEventType.Updated)); - - // Parse files into ParserInfos - if (fileCount < 100) - { - // Process files sequentially - result.ParserInfos = files - .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata)) - .Where(info => info != null) - .ToList()!; + nonLocalizedSeries = nonLocalizedSeriesFound[0]; } else { - // Process files in parallel - var tasks = files.Select(file => Task.Run(() => - _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata))); - - var infos = await Task.WhenAll(tasks); - result.ParserInfos = infos.Where(info => info != null).ToList()!; + // There can be a case where there are multiple series in a folder that causes merging. + if (nonLocalizedSeriesFound.Count > 2) + { + _logger.LogError("[ScannerService] There are multiple series within one folder that contain localized series. This will cause them to group incorrectly. Please separate series into their own dedicated folder or ensure there is only 2 potential series (localized and series): {LocalizedSeries}", string.Join(", ", nonLocalizedSeriesFound)); + } + nonLocalizedSeries = nonLocalizedSeriesFound.Find(s => !s.Equals(localizedSeries)); } - } + if (nonLocalizedSeries == null) return; - private static void UpdateSortOrder(ConcurrentDictionary> scannedSeries, ParsedSeries series) - { - // Set the Sort order per Volume - var volumes = scannedSeries[series].GroupBy(info => info.Volumes); - foreach (var volume in volumes) + var normalizedNonLocalizedSeries = nonLocalizedSeries.ToNormalized(); + foreach (var infoNeedingMapping in infos.Where(i => + !i.Series.ToNormalized().Equals(normalizedNonLocalizedSeries))) { - var infos = scannedSeries[series].Where(info => info.Volumes == volume.Key).ToList(); - IList chapters; - var specialTreatment = infos.TrueForAll(info => info.IsSpecial); - var hasAnySpMarker = infos.Exists(info => info.SpecialIndex > 0); - var counter = 0f; - - // Handle specials with SpecialIndex - if (specialTreatment && hasAnySpMarker) - { - chapters = infos - .OrderBy(info => info.SpecialIndex) - .ToList(); - - foreach (var chapter in chapters) - { - chapter.IssueOrder = counter; - counter++; - } - continue; - } - - // Handle specials without SpecialIndex (natural order) - if (specialTreatment) - { - chapters = infos - .OrderByNatural(info => Parser.Parser.RemoveExtensionIfSupported(info.Filename)!) - .ToList(); - - foreach (var chapter in chapters) - { - chapter.IssueOrder = counter; - counter++; - } - continue; - } - - // Ensure chapters are sorted numerically when possible, otherwise push unparseable to the end - chapters = infos - .OrderBy(info => float.TryParse(info.Chapters, NumberStyles.Any, CultureInfo.InvariantCulture, out var val) ? val : float.MaxValue) - .ToList(); - - counter = 0f; - var prevIssue = string.Empty; - foreach (var chapter in chapters) - { - // Use MinNumber in case there is a range, as otherwise sort order will cause it to be processed last - var chapterNum = - $"{Parser.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; - if (float.TryParse(chapterNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter)) - { - // Parsed successfully, use the numeric value - counter = parsedChapter; - chapter.IssueOrder = counter; - - // Increment for next chapter (unless the next has a similar value, then add 0.1) - if (!string.IsNullOrEmpty(prevIssue) && float.TryParse(prevIssue, NumberStyles.Any, CultureInfo.InvariantCulture, out var prevIssueFloat) && parsedChapter.Is(prevIssueFloat)) - { - counter += 0.1f; // bump if same value as the previous issue - } - prevIssue = $"{parsedChapter.ToString(CultureInfo.InvariantCulture)}"; - } - else - { - // Unparsed chapters: use the current counter and bump for the next - if (!string.IsNullOrEmpty(prevIssue) && prevIssue == counter.ToString(CultureInfo.InvariantCulture)) - { - counter += 0.1f; // bump if same value as the previous issue - } - chapter.IssueOrder = counter; - counter++; - prevIssue = chapter.Chapters; - } - } + infoNeedingMapping.Series = nonLocalizedSeries; + infoNeedingMapping.LocalizedSeries = localizedSeries; } } } diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/API/Services/Tasks/Scanner/Parser/BasicParser.cs deleted file mode 100644 index 168ca7f01..000000000 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; - -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable - -/// -/// This is the basic parser for handling Manga/Comic/Book libraries. This was previously DefaultParser before splitting each parser -/// into their own classes. -/// -public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService) -{ - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) - { - var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); - // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. - if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; - - if (Parser.IsImage(filePath)) - { - return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); - } - - var ret = new ParserInfo() - { - Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = Parser.ParseSeries(fileName, type), - ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type), - }; - - if (ret.Series == string.Empty || Parser.IsImage(filePath)) - { - // Try to parse information out of each folder all the way to rootPath - ParseFromFallbackFolders(filePath, rootPath, type, ref ret); - } - - var edition = Parser.ParseEdition(fileName); - if (!string.IsNullOrEmpty(edition)) - { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); - ret.Edition = edition; - } - - var isSpecial = Parser.IsSpecial(fileName, type); - // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that - // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial) - { - ret.IsSpecial = true; - ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder - } - - // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Parser.HasSpecialMarker(fileName)) - { - ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; - - // NOTE: This uses rootPath. LibraryRoot works better for manga, but it's not always that way. - // It might be worth writing some logic if the file is a special, to take the folder above the Specials/ - // if present - var tempRootPath = rootPath; - if (rootPath.EndsWith("Specials") || rootPath.EndsWith("Specials/")) - { - tempRootPath = rootPath.Replace("Specials", string.Empty).TrimEnd('/'); - } - - // Check if the folder the file exists in is Specials/ and if so, take the parent directory as series (cleaned) - var fileDirectory = Path.GetDirectoryName(filePath); - if (!string.IsNullOrEmpty(fileDirectory) && - (fileDirectory.EndsWith("Specials", StringComparison.OrdinalIgnoreCase) || - fileDirectory.EndsWith("Specials/", StringComparison.OrdinalIgnoreCase))) - { - ret.Series = Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); - } - else - { - ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); - } - ret.Title = Parser.CleanSpecialTitle(fileName); - } - - if (string.IsNullOrEmpty(ret.Series)) - { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); - } - - // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) - { - ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); - } - - // Patch in other information from ComicInfo - if (enableMetadata) - { - UpdateFromComicInfo(ret); - } - - - - if (ret.Volumes == Parser.LooseLeafVolume && ret.Chapters == Parser.DefaultChapter) - { - ret.IsSpecial = true; - } - - // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number - if (ret.IsSpecial) - { - ret.Volumes = Parser.SpecialVolume; - } - - return ret.Series == string.Empty ? null : ret; - } - - /// - /// Applicable for everything but ComicVine and Image library types - /// - /// - /// - /// - public override bool IsApplicable(string filePath, LibraryType type) - { - return type != LibraryType.ComicVine && type != LibraryType.Image; - } -} diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/API/Services/Tasks/Scanner/Parser/BookParser.cs deleted file mode 100644 index 14f42c989..000000000 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ /dev/null @@ -1,62 +0,0 @@ -using API.Data.Metadata; -using API.Entities.Enums; - -namespace API.Services.Tasks.Scanner.Parser; - -public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) -{ - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) - { - var info = bookService.ParseInfo(filePath); - if (info == null) return null; - - info.ComicInfo = comicInfo; - - // We need a special piece of code to override the Series IF there is a special marker in the filename for epub files - if (info.IsSpecial && info.Volumes is "0" or "0.0" && info.ComicInfo.Series != info.Series) - { - info.Series = info.ComicInfo.Series; - } - - // This catches when original library type is Manga/Comic and when parsing with non - if (Parser.ParseVolume(info.Series, type) != Parser.LooseLeafVolume) - { - var hasVolumeInTitle = !Parser.ParseVolume(info.Title, type) - .Equals(Parser.LooseLeafVolume); - var hasVolumeInSeries = !Parser.ParseVolume(info.Series, type) - .Equals(Parser.LooseLeafVolume); - - if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) - { - // NOTE: I'm not sure the comment is true. I've never seen this triggered - // This is likely a light novel for which we can set series from parsed title - info.Series = Parser.ParseSeries(info.Title, type); - info.Volumes = Parser.ParseVolume(info.Title, type); - } - else - { - var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo); - info.Merge(info2); - if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type) - .Equals(Parser.LooseLeafVolume)) - { - // Override the Series name so it groups appropriately - info.Series = info2.Series; - } - } - } - - return string.IsNullOrEmpty(info.Series) ? null : info; - } - - /// - /// Only applicable for Epub files - /// - /// - /// - /// - public override bool IsApplicable(string filePath, LibraryType type) - { - return Parser.IsEpub(filePath); - } -} diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs deleted file mode 100644 index b60f28aee..000000000 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.IO; -using System.Linq; -using API.Data.Metadata; -using API.Entities.Enums; - -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable - -/// -/// Responsible for Parsing ComicVine Comics. -/// -/// -public class ComicVineParser(IDirectoryService directoryService) : DefaultParser(directoryService) -{ - /// - /// This Parser generates Series name to be defined as Series + first Issue Volume, so "Batman (2020)". - /// - /// - /// - /// - /// - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) - { - if (type != LibraryType.ComicVine) return null; - - var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); - // Mylar often outputs cover.jpg, ignore it by default - if (string.IsNullOrEmpty(fileName) || Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; - - var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - - var info = new ParserInfo() - { - Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = string.Empty, - ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type) - }; - - // See if we can formulate the name from the ComicInfo - if (!string.IsNullOrEmpty(info.ComicInfo?.Series) && !string.IsNullOrEmpty(info.ComicInfo?.Volume)) - { - info.Series = $"{info.ComicInfo.Series} ({info.ComicInfo.Volume})"; - } - - if (string.IsNullOrEmpty(info.Series)) - { - // Check if we need to fallback to the Folder name AND that the folder matches the format "Series (Year)" - var directories = directoryService.GetFoldersTillRoot(rootPath, filePath).ToList(); - if (directories.Count > 0) - { - foreach (var directory in directories) - { - if (!Parser.IsSeriesAndYear(directory)) continue; - info.Series = directory; - info.Volumes = Parser.ParseYear(directory); - break; - } - - // When there was at least one directory and we failed to parse the series, this is the final fallback - if (string.IsNullOrEmpty(info.Series)) - { - info.Series = Parser.CleanTitle(directories[0], true); - } - } - else - { - if (Parser.IsSeriesAndYear(directoryName)) - { - info.Series = directoryName; - info.Volumes = Parser.ParseYear(directoryName); - } - } - } - - // Check if this is a Special/Annual - info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); - - // Patch in other information from ComicInfo - if (enableMetadata) - { - UpdateFromComicInfo(info); - } - - if (string.IsNullOrEmpty(info.Series)) - { - info.Series = Parser.CleanTitle(directoryName, true); - } - - - return string.IsNullOrEmpty(info.Series) ? null : info; - } - - /// - /// Only applicable for ComicVine library type - /// - /// - /// - /// - public override bool IsApplicable(string filePath, LibraryType type) - { - return type == LibraryType.ComicVine; - } - - private new static void UpdateFromComicInfo(ParserInfo info) - { - if (info.ComicInfo == null) return; - - if (!string.IsNullOrEmpty(info.ComicInfo.Volume)) - { - info.Volumes = info.ComicInfo.Volume; - } - if (string.IsNullOrEmpty(info.LocalizedSeries) && !string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries)) - { - info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); - } - if (!string.IsNullOrEmpty(info.ComicInfo.Number)) - { - info.Chapters = info.ComicInfo.Number; - if (info.IsSpecial && Parser.DefaultChapter != info.Chapters) - { - info.IsSpecial = false; - info.Volumes = $"{Parser.SpecialVolumeNumber}"; - } - } - - // Patch is SeriesSort from ComicInfo - if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort)) - { - info.SeriesSort = info.ComicInfo.TitleSort.Trim(); - } - } -} diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 687617fd7..188afc9c1 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -1,34 +1,123 @@ using System.IO; using System.Linq; -using API.Data.Metadata; using API.Entities.Enums; namespace API.Services.Tasks.Scanner.Parser; -#nullable enable public interface IDefaultParser { - ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); + ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); - bool IsApplicable(string filePath, LibraryType type); } /// /// This is an implementation of the Parser that is the basis for everything /// -public abstract class DefaultParser(IDirectoryService directoryService) : IDefaultParser +public class DefaultParser : IDefaultParser { + private readonly IDirectoryService _directoryService; + + public DefaultParser(IDirectoryService directoryService) + { + _directoryService = directoryService; + } /// - /// Parses information out of a file path. Can fallback to using directory name if Series couldn't be parsed + /// Parses information out of a file path. Will fallback to using directory name if Series couldn't be parsed /// from filename. /// /// /// Root folder - /// Allows different Regex to be used for parsing. - /// Allows overriding data from metadata (ComicInfo/pdf/epub) + /// Defaults to Manga. Allows different Regex to be used for parsing. /// or null if Series was empty - public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); + public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) + { + var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); + // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. (we can probably remove this and have users use kavitaignore) + if (Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null; + + ParserInfo ret; + + if (Parser.IsEpub(filePath)) // NOTE: Will this ever be called? Because we use ReadingService to handle parse + { + ret = new ParserInfo + { + Chapters = Parser.ParseChapter(fileName) ?? Parser.ParseComicChapter(fileName), + Series = Parser.ParseSeries(fileName) ?? Parser.ParseComicSeries(fileName), + Volumes = Parser.ParseVolume(fileName) ?? Parser.ParseComicVolume(fileName), + Filename = Path.GetFileName(filePath), + Format = Parser.ParseFormat(filePath), + FullFilePath = filePath + }; + } + else + { + ret = new ParserInfo + { + Chapters = type == LibraryType.Comic ? Parser.ParseComicChapter(fileName) : Parser.ParseChapter(fileName), + Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName), + Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName), + Filename = Path.GetFileName(filePath), + Format = Parser.ParseFormat(filePath), + Title = Path.GetFileNameWithoutExtension(fileName), + FullFilePath = filePath + }; + } + + + if (Parser.IsImage(filePath)) + { + // Reset Chapters, Volumes, and Series as images are not good to parse information out of. Better to use folders. + ret.Volumes = Parser.DefaultVolume; + ret.Chapters = Parser.DefaultChapter; + ret.Series = string.Empty; + } + + if (ret.Series == string.Empty || Parser.IsImage(filePath)) + { + // Try to parse information out of each folder all the way to rootPath + ParseFromFallbackFolders(filePath, rootPath, type, ref ret); + } + + var edition = Parser.ParseEdition(fileName); + if (!string.IsNullOrEmpty(edition)) + { + ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic); + ret.Edition = edition; + } + + var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName); + // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that + // could cause a problem as Omake is a special term, but there is valid volume/chapter information. + if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.DefaultVolume && isSpecial) + { + ret.IsSpecial = true; + ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder + } + + // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name + if (Parser.HasSpecialMarker(fileName)) + { + ret.IsSpecial = true; + ret.Chapters = Parser.DefaultChapter; + ret.Volumes = Parser.DefaultVolume; + + ParseFromFallbackFolders(filePath, rootPath, type, ref ret); + } + + if (string.IsNullOrEmpty(ret.Series)) + { + ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); + } + + // Pdfs may have .pdf in the series name, remove that + if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + { + ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); + } + + return ret.Series == string.Empty ? null : ret; + } /// /// Fills out by trying to parse volume, chapters, and series from folders @@ -39,14 +128,14 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau /// Expects a non-null ParserInfo which this method will populate public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret) { - var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath) - .Where(f => !Parser.IsSpecial(f, type)) + var fallbackFolders = _directoryService.GetFoldersTillRoot(rootPath, filePath) + .Where(f => !Parser.IsMangaSpecial(f)) .ToList(); if (fallbackFolders.Count == 0) { - var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - var series = Parser.ParseSeries(rootFolderName, type); + var rootFolderName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; + var series = Parser.ParseSeries(rootFolderName); if (string.IsNullOrEmpty(series)) { @@ -65,18 +154,16 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau { var folder = fallbackFolders[i]; - var parsedVolume = Parser.ParseVolume(folder, type); - var parsedChapter = Parser.ParseChapter(folder, type); + var parsedVolume = type is LibraryType.Manga ? Parser.ParseVolume(folder) : Parser.ParseComicVolume(folder); + var parsedChapter = type is LibraryType.Manga ? Parser.ParseChapter(folder) : Parser.ParseComicChapter(folder); - if (!parsedVolume.Equals(Parser.LooseLeafVolume) || !parsedChapter.Equals(Parser.DefaultChapter)) + if (!parsedVolume.Equals(Parser.DefaultVolume) || !parsedChapter.Equals(Parser.DefaultChapter)) { - if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.LooseLeafVolume)) - && !string.IsNullOrEmpty(parsedVolume) && !parsedVolume.Equals(Parser.LooseLeafVolume)) + if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.DefaultVolume)) && !parsedVolume.Equals(Parser.DefaultVolume)) { ret.Volumes = parsedVolume; } - if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) - && !string.IsNullOrEmpty(parsedChapter) && !parsedChapter.Equals(Parser.DefaultChapter)) + if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) && !parsedChapter.Equals(Parser.DefaultChapter)) { ret.Chapters = parsedChapter; } @@ -85,7 +172,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau // Generally users group in series folders. Let's try to parse series from the top folder if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1) { - var series = Parser.ParseSeries(folder, type); + var series = Parser.ParseSeries(folder); if (string.IsNullOrEmpty(series)) { @@ -93,7 +180,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau break; } - if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) && !folder.Contains(ret.Series))) + if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !folder.Contains(ret.Series))) { ret.Series = series; break; @@ -101,48 +188,4 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau } } } - - protected static void UpdateFromComicInfo(ParserInfo info) - { - if (info.ComicInfo == null) return; - - if (!string.IsNullOrEmpty(info.ComicInfo.Volume)) - { - info.Volumes = info.ComicInfo.Volume; - } - if (!string.IsNullOrEmpty(info.ComicInfo.Number)) - { - info.Chapters = info.ComicInfo.Number; - } - if (!string.IsNullOrEmpty(info.ComicInfo.Series)) - { - info.Series = info.ComicInfo.Series.Trim(); - } - if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries)) - { - info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); - } - - if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) - { - info.IsSpecial = true; - info.Chapters = Parser.DefaultChapter; - info.Volumes = Parser.SpecialVolume; - } - - // Patch is SeriesSort from ComicInfo - if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort)) - { - info.SeriesSort = info.ComicInfo.SeriesSort.Trim(); - } - - } - - public abstract bool IsApplicable(string filePath, LibraryType type); - - protected static bool IsEmptyOrDefault(string volumes, string chapters) - { - return (string.IsNullOrEmpty(chapters) || chapters == Parser.DefaultChapter) && - (string.IsNullOrEmpty(volumes) || volumes == Parser.LooseLeafVolume); - } } diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/API/Services/Tasks/Scanner/Parser/ImageParser.cs deleted file mode 100644 index 12f9f4d50..000000000 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; - -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable - -public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) -{ - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) - { - if (!IsApplicable(filePath, type)) return null; - - var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); - var ret = new ParserInfo - { - Series = directoryName, - Volumes = Parser.LooseLeafVolume, - Chapters = Parser.DefaultChapter, - ComicInfo = comicInfo, - Format = MangaFormat.Image, - Filename = Path.GetFileName(filePath), - FullFilePath = Parser.NormalizePath(filePath), - Title = fileName, - }; - ParseFromFallbackFolders(filePath, libraryRoot, LibraryType.Image, ref ret); - - if (IsEmptyOrDefault(ret.Volumes, ret.Chapters)) - { - ret.IsSpecial = true; - ret.Volumes = Parser.SpecialVolume; - } - - // Override the series name, as fallback folders needs it to try and parse folder name - if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName)) - { - ret.Series = Parser.CleanTitle(directoryName); - } - - - return string.IsNullOrEmpty(ret.Series) ? null : ret; - } - - /// - /// Only applicable for Image files and Image library type - /// - /// - /// - /// - public override bool IsApplicable(string filePath, LibraryType type) - { - return type == LibraryType.Image && Parser.IsImage(filePath); - } -} diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index c0b130f91..187c671ed 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -7,28 +7,16 @@ using API.Entities.Enums; using API.Extensions; namespace API.Services.Tasks.Scanner.Parser; -#nullable enable -public static partial class Parser +public static class Parser { - // NOTE: If you change this, don't forget to change in the UI (see Series Detail) - public const string DefaultChapter = "-100000"; // -2147483648 - public const string LooseLeafVolume = "-100000"; - public const int DefaultChapterNumber = -100_000; - public const int LooseLeafVolumeNumber = -100_000; - /// - /// The Volume Number of Specials to reside in - /// - public const int SpecialVolumeNumber = 100_000; - public const string SpecialVolume = "100000"; - + public const string DefaultChapter = "0"; + public const string DefaultVolume = "0"; public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser + public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; - public const string EpubFileExtension = @"\.epub"; - public const string PdfFileExtension = @"\.pdf"; - private const string BookFileExtensions = EpubFileExtension + "|" + PdfFileExtension; + private const string BookFileExtensions = @"\.epub|\.pdf"; private const string XmlRegexExtensions = @"\.xml"; public const string MacOsMetadataFileStartsWith = @"._"; @@ -43,94 +31,84 @@ public static partial class Parser "One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel", "GN", "FCBD", "Giant Size"); - private static readonly char[] LeadingZeroesTrimChars = ['0']; + private static readonly char[] LeadingZeroesTrimChars = new[] { '0' }; - private static readonly char[] SpacesAndSeparators = ['\0', '\t', '\r', ' ', '-', ',']; + private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','}; private const string Number = @"\d+(\.\d)?"; private const string NumberRange = Number + @"(-" + Number + @")?"; /// - /// non-greedy matching of a string where parenthesis are balanced + /// non greedy matching of a string where parenthesis are balanced /// public const string BalancedParen = @"(?:[^()]|(?\()|(?<-open>\)))*?(?(open)(?!))"; /// - /// non-greedy matching of a string where square brackets are balanced + /// non greedy matching of a string where square brackets are balanced /// public const string BalancedBracket = @"(?:[^\[\]]|(?\[)|(?<-open>\]))*?(?(open)(?!))"; /// /// Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ] /// private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(? + /// Common regex patterns present in both Comics and Mangas + /// + private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus"; /// /// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data /// /// See here for some examples https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face - public static readonly Regex FontSrcUrlRegex = new(@"(?(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))" - + "(?(?!data:)[^\"']+?)" + "(?[\"']?" + @"\);?)", + public static readonly Regex FontSrcUrlRegex = new Regex(@"(?(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))" + + "(?(?!data:)[^\"']+?)" + "(?[\"']?" + @"\);?)", MatchOptions, RegexTimeout); /// /// https://developer.mozilla.org/en-US/docs/Web/CSS/@import /// - public static readonly Regex CssImportUrlRegex = new("(@import\\s([\"|']|url\\([\"|']))(?[^'\"]+)([\"|']\\)?);", + public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s([\"|']|url\\([\"|']))(?[^'\"]+)([\"|']\\)?);", MatchOptions | RegexOptions.Multiline, RegexTimeout); /// /// Misc css image references, like background-image: url(), border-image, or list-style-image /// /// Original prepend: (background|border|list-style)-image:\s?)? - public static readonly Regex CssImageUrlRegex = new(@"(url\((?!data:).(?!data:))" + "(?(?!data:)[^\"']*)" + @"(.\))", + public static readonly Regex CssImageUrlRegex = new Regex(@"(url\((?!data:).(?!data:))" + "(?(?!data:)[^\"']*)" + @"(.\))", MatchOptions, RegexTimeout); - private static readonly Regex ImageRegex = new(ImageFileExtensions, + private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, MatchOptions, RegexTimeout); - private static readonly Regex ArchiveFileRegex = new(ArchiveFileExtensions, + private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, MatchOptions, RegexTimeout); - private static readonly Regex ComicInfoArchiveRegex = new(@"\.cbz|\.cbr|\.cb7|\.cbt", + private static readonly Regex ComicInfoArchiveRegex = new Regex(@"\.cbz|\.cbr|\.cb7|\.cbt", MatchOptions, RegexTimeout); - private static readonly Regex XmlRegex = new(XmlRegexExtensions, + private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, MatchOptions, RegexTimeout); - private static readonly Regex BookFileRegex = new(BookFileExtensions, + private static readonly Regex BookFileRegex = new Regex(BookFileExtensions, MatchOptions, RegexTimeout); - private static readonly Regex CoverImageRegex = new(@"(? - /// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be - /// added on a case-by-case basis. - /// - private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]", - MatchOptions, RegexTimeout); - - /// - /// Supports Batman (2020) or Batman (2) - /// - private static readonly Regex SeriesAndYearRegex = new(@"^\D+\s\((?\d+)\)$", + private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+!]", MatchOptions, RegexTimeout); /// /// Recognizes the Special token only /// - private static readonly Regex SpecialTokenRegex = new(@"SP\d+", + private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+", MatchOptions, RegexTimeout); - private static readonly Regex[] MangaVolumeRegex = - [ - // Thai Volume: เล่ม n -> Volume n - new Regex( - @"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?\d+(\-\d+)?(\.\d+)?)", - MatchOptions, RegexTimeout), + private static readonly Regex[] MangaVolumeRegex = new[] + { // Dance in the Vampire Bund v16-17 new Regex( @"(?.*)(\b|_)v(?\d+-?\d+)( |_)", MatchOptions, RegexTimeout), - // Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake + // NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar new Regex( - @"^(?.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?\d+(\.\d+)?)(.+?|$)", + @"(?.*)(\b|_)(?!\[)(vol\.?)(?\d+(-\d+)?)(?!\])", MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 new Regex( @@ -156,7 +134,6 @@ public static partial class Parser new Regex( @"(vol_)(?\d+(\.\d)?)", MatchOptions, RegexTimeout), - // Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册 new Regex( @"第(?\d+)(卷|册)", @@ -165,9 +142,9 @@ public static partial class Parser new Regex( @"(卷|册)(?\d+)", MatchOptions, RegexTimeout), - // Korean Volume: 제n화|회|장 -> Volume n, n화|권|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) + // Korean Volume: 제n화|권|회|장 -> Volume n, n화|권|회|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) new Regex( - @"제?(?\d+(\.\d+)?)(권|화|장)", + @"제?(?\d+(\.\d)?)(권|회|화|장)", MatchOptions, RegexTimeout), // Korean Season: 시즌n -> Season n, new Regex( @@ -192,15 +169,11 @@ public static partial class Parser // Russian Volume: n Том -> Volume n new Regex( @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", - MatchOptions, RegexTimeout) - ]; - - private static readonly Regex[] MangaSeriesRegex = - [ - // Thai Volume: เล่ม n -> Volume n - new Regex( - @"(?.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?\d+(\-\d+)?(\.\d+)?)", MatchOptions, RegexTimeout), + }; + + private static readonly Regex[] MangaSeriesRegex = new[] + { // Russian Volume: Том n -> Volume n, Тома n -> Volume new Regex( @"(?.+?)Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", @@ -221,17 +194,16 @@ public static partial class Parser new Regex( @"(?.*)(\b|_|-|\s)(?:sp)\d", MatchOptions, RegexTimeout), + // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz + new Regex( + @"^(?.*)( |_)Vol\.?(\d+|tbd)", + MatchOptions, RegexTimeout), // Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex), // The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake new Regex( @"(?.+?)(\s|_|-)+(?:Vol(ume|\.)?(\s|_|-)+\d+)(\s|_|-)+(?:(Ch|Chapter|Ch)\.?)(\s|_|-)+(?\d+)", MatchOptions, RegexTimeout), - // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz, - // Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake - new Regex( - @"^(?.+?)(?:\s*|_|\-\s*)+(?:Ch(?:apter|\.|)\s*\d+(?:\.\d+)?(?:\s*|_|\-\s*)+)?Vol(?:ume|\.|)\s*(?:\d+|tbd)(?:\s|_|\-\s*).+", - MatchOptions, RegexTimeout), // Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip new Regex( @"(?.*)(\b|_)v(?\d+-?\d*)(\s|_|-)", @@ -239,7 +211,7 @@ public static partial class Parser RegexTimeout), // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] new Regex( - @"(?.+?)( - )(?:v|vo|c|chapters)\d", + @"(?.*)( - )(?:v|vo|c|chapters)\d", MatchOptions, RegexTimeout), // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip new Regex( @@ -258,7 +230,6 @@ public static partial class Parser @"(?.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)", MatchOptions, RegexTimeout), - // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] new Regex( @"(?.+?):? (\b|_|-)(vol)(ume)", @@ -266,7 +237,7 @@ public static partial class Parser RegexTimeout), //Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans] new Regex( - @"(?.*?)(?.*)(\bc\d+\b)", MatchOptions, RegexTimeout), //Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz new Regex( @@ -369,16 +340,12 @@ public static partial class Parser // Japanese Volume: n巻 -> Volume n new Regex( @"(?.+?)第(?\d+(?:(\-)\d+)?)巻", - MatchOptions, RegexTimeout) - - ]; - - private static readonly Regex[] ComicSeriesRegex = - [ - // Thai Volume: เล่ม n -> Volume n - new Regex( - @"(?.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?\d+(\-\d+)?(\.\d+)?)", MatchOptions, RegexTimeout), + + }; + + private static readonly Regex[] ComicSeriesRegex = new[] + { // Russian Volume: Том n -> Volume n, Тома n -> Volume new Regex( @"(?.+?)Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", @@ -462,15 +429,11 @@ public static partial class Parser // MUST BE LAST: Batman & Daredevil - King of New York new Regex( @"^(?.*)", - MatchOptions, RegexTimeout) - ]; - - private static readonly Regex[] ComicVolumeRegex = - [ - // Thai Volume: เล่ม n -> Volume n - new Regex( - @"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?\d+(\-\d+)?(\.\d+)?)", MatchOptions, RegexTimeout), + }; + + private static readonly Regex[] ComicVolumeRegex = new[] + { // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.+?)(?: |_)(t|v)(?" + NumberRange + @")", @@ -502,15 +465,11 @@ public static partial class Parser // Russian Volume: n Том -> Volume n new Regex( @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", - MatchOptions, RegexTimeout) - ]; - - private static readonly Regex[] ComicChapterRegex = - [ - // Thai Volume: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n - new Regex( - @"(บทที่|ตอนที่)(\s)?(\.?)(\s|_)?(?\d+(\-\d+)?(\.\d+)?)", MatchOptions, RegexTimeout), + }; + + private static readonly Regex[] ComicChapterRegex = new[] + { // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", @@ -571,18 +530,14 @@ public static partial class Parser // spawn-123, spawn-chapter-123 (from https://github.com/Girbons/comics-downloader) new Regex( @"^(?.+?)-(chapter-)?(?\d+)", - MatchOptions, RegexTimeout) - ]; - - private static readonly Regex[] MangaChapterRegex = - [ - // Thai Chapter: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n, เล่ม n -> Volume n, เล่มที่ n -> Volume n - new Regex( - @"(?((เล่ม|เล่มที่))?(\s|_)?\.?\d+)(\s|_)(บทที่|ตอนที่)\.?(\s|_)?(?\d+)", MatchOptions, RegexTimeout), + }; + + private static readonly Regex[] MangaChapterRegex = new[] + { // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 new Regex( - @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)(-c?\d+(\.\d)?)?)", + @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", MatchOptions, RegexTimeout), // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @@ -640,8 +595,8 @@ public static partial class Parser // Russian Chapter: n Главa -> Chapter n new Regex( @"(?!Том)(?\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)", - MatchOptions, RegexTimeout) - ]; + MatchOptions, RegexTimeout), + }; private static readonly Regex MangaEditionRegex = new Regex( // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz @@ -656,6 +611,25 @@ public static partial class Parser MatchOptions, RegexTimeout ); + private static readonly Regex MangaSpecialRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + $@"\b(?:{CommonSpecial}|Omake)\b", + MatchOptions, RegexTimeout + ); + + private static readonly Regex ComicSpecialRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + $@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b", + MatchOptions, RegexTimeout + ); + + private static readonly Regex EuropeanComicRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + @"\b(?:Bd[-\s]Fr)\b", + MatchOptions, RegexTimeout + ); + + // If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found. private static readonly Regex SpecialMarkerRegex = new Regex( @"SP\d+", @@ -690,25 +664,24 @@ public static partial class Parser /// /// /// - public static bool HasSpecialMarker(string? filePath) + public static bool HasSpecialMarker(string filePath) { - if (string.IsNullOrEmpty(filePath)) return false; return SpecialMarkerRegex.IsMatch(filePath); } - public static int ParseSpecialIndex(string filePath) + public static bool IsMangaSpecial(string filePath) { - var match = SpecialMarkerRegex.Match(filePath).Value.Replace("SP", string.Empty); - if (string.IsNullOrEmpty(match)) return 0; - return int.Parse(match); + filePath = ReplaceUnderscores(filePath); + return MangaSpecialRegex.IsMatch(filePath); } - public static bool IsSpecial(string? filePath, LibraryType type) + public static bool IsComicSpecial(string filePath) { - return HasSpecialMarker(filePath); + filePath = ReplaceUnderscores(filePath); + return ComicSpecialRegex.IsMatch(filePath); } - private static string ParseMangaSeries(string filename) + public static string ParseSeries(string filename) { foreach (var regex in MangaSeriesRegex) { @@ -716,11 +689,7 @@ public static partial class Parser var group = matches .Select(match => match.Groups["Series"]) .FirstOrDefault(group => group.Success && group != Match.Empty); - - if (group != null) - { - return CleanTitle(group.Value); - } + if (group != null) return CleanTitle(group.Value); } return string.Empty; @@ -739,7 +708,7 @@ public static partial class Parser return string.Empty; } - public static string ParseMangaVolume(string filename) + public static string ParseVolume(string filename) { foreach (var regex in MangaVolumeRegex) { @@ -754,7 +723,7 @@ public static partial class Parser } } - return LooseLeafVolume; + return DefaultVolume; } public static string ParseComicVolume(string filename) @@ -772,10 +741,9 @@ public static partial class Parser } } - return LooseLeafVolume; + return DefaultVolume; } - private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-')) @@ -785,61 +753,13 @@ public static partial class Parser var tokens = value.Split("-"); var from = RemoveLeadingZeroes(tokens[0]); - if (tokens.Length != 2) return from; - // Occasionally users will use c01-c02 instead of c01-02, clean any leftover c - if (tokens[1].StartsWith("c", StringComparison.InvariantCultureIgnoreCase)) - { - tokens[1] = tokens[1].Replace("c", string.Empty, StringComparison.InvariantCultureIgnoreCase); - } var to = RemoveLeadingZeroes(hasPart ? AddChapterPart(tokens[1]) : tokens[1]); return $"{from}-{to}"; } - public static string ParseSeries(string filename, LibraryType type) - { - return type switch - { - LibraryType.Manga => ParseMangaSeries(filename), - LibraryType.Comic => ParseComicSeries(filename), - LibraryType.Book => ParseMangaSeries(filename), - LibraryType.Image => ParseMangaSeries(filename), - LibraryType.LightNovel => ParseMangaSeries(filename), - LibraryType.ComicVine => ParseComicSeries(filename), - _ => string.Empty - }; - } - - public static string ParseVolume(string filename, LibraryType type) - { - return type switch - { - LibraryType.Manga => ParseMangaVolume(filename), - LibraryType.Comic => ParseComicVolume(filename), - LibraryType.Book => ParseMangaVolume(filename), - LibraryType.Image => ParseMangaVolume(filename), - LibraryType.LightNovel => ParseMangaVolume(filename), - LibraryType.ComicVine => ParseComicVolume(filename), - _ => LooseLeafVolume - }; - } - - public static string ParseChapter(string filename, LibraryType type) - { - return type switch - { - LibraryType.Manga => ParseMangaChapter(filename), - LibraryType.Comic => ParseComicChapter(filename), - LibraryType.Book => ParseMangaChapter(filename), - LibraryType.Image => ParseMangaChapter(filename), - LibraryType.LightNovel => ParseMangaChapter(filename), - LibraryType.ComicVine => ParseComicChapter(filename), - _ => DefaultChapter - }; - } - - private static string ParseMangaChapter(string filename) + public static string ParseChapter(string filename) { foreach (var regex in MangaChapterRegex) { @@ -868,7 +788,7 @@ public static partial class Parser return $"{value}.5"; } - private static string ParseComicChapter(string filename) + public static string ParseComicChapter(string filename) { foreach (var regex in ComicChapterRegex) { @@ -895,6 +815,22 @@ public static partial class Parser return title; } + private static string RemoveMangaSpecialTags(string title) + { + return MangaSpecialRegex.Replace(title, string.Empty); + } + + private static string RemoveEuropeanTags(string title) + { + return EuropeanComicRegex.Replace(title, string.Empty); + } + + private static string RemoveComicSpecialTags(string title) + { + return ComicSpecialRegex.Replace(title, string.Empty); + } + + /// /// Translates _ -> spaces, trims front and back of string, removes release groups @@ -913,6 +849,16 @@ public static partial class Parser title = RemoveEditionTagHolders(title); + if (isComic) + { + title = RemoveComicSpecialTags(title); + title = RemoveEuropeanTags(title); + } + else + { + title = RemoveMangaSpecialTags(title); + } + title = title.Trim(SpacesAndSeparators); title = EmptySpaceRegex.Replace(title, " "); @@ -980,52 +926,35 @@ public static partial class Parser { try { - // Check if the range string is not null or empty - if (string.IsNullOrEmpty(range) || !Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) + if (!Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) { - return 0.0f; + return (float) 0.0; } - // Check if there is a range or not - if (NumberRangeRegex().IsMatch(range)) - { - - var tokens = range.Replace("_", string.Empty).Split("-", StringSplitOptions.RemoveEmptyEntries); - return tokens.Min(t => t.AsFloat()); - } - - return range.AsFloat(); + var tokens = range.Replace("_", string.Empty).Split("-"); + return tokens.Min(t => t.AsFloat()); } - catch (Exception) + catch { - return 0.0f; + return (float) 0.0; } } - public static float MaxNumberFromRange(string range) { try { - // Check if the range string is not null or empty - if (string.IsNullOrEmpty(range) || !Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) + if (!Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) { - return 0.0f; + return (float) 0.0; } - // Check if there is a range or not - if (NumberRangeRegex().IsMatch(range)) - { - - var tokens = range.Replace("_", string.Empty).Split("-", StringSplitOptions.RemoveEmptyEntries); - return tokens.Max(t => t.AsFloat()); - } - - return range.AsFloat(); + var tokens = range.Replace("_", string.Empty).Split("-"); + return tokens.Max(t => t.AsFloat()); } - catch (Exception) + catch { - return 0.0f; + return (float) 0.0; } } @@ -1043,6 +972,11 @@ public static partial class Parser { if (string.IsNullOrEmpty(name)) return name; var cleaned = SpecialTokenRegex.Replace(name.Replace('_', ' '), string.Empty).Trim(); + var lastIndex = cleaned.LastIndexOf('.'); + if (lastIndex > 0) + { + cleaned = cleaned.Substring(0, cleaned.LastIndexOf('.')).Trim(); + } return string.IsNullOrEmpty(cleaned) ? name : cleaned; } @@ -1060,7 +994,7 @@ public static partial class Parser } /// - /// Validates that a Path doesn't start with certain blacklisted folders, like __MACOSX, @Recently-Snapshot, etc. and that if a full path, the filename + /// Validates that a Path doesn't start with certain blacklisted folders, like __MACOSX, @Recently-Snapshot, etc and that if a full path, the filename /// doesn't start with ._, which is a metadata file on MACOSX. /// /// @@ -1070,7 +1004,6 @@ public static partial class Parser return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._") || Path.GetFileName(path).StartsWith("._") || path.Contains(".qpkg") || path.StartsWith("#recycle") - || path.Contains(".yacreaderlibrary") || path.Contains(".caltrash"); } @@ -1143,50 +1076,9 @@ public static partial class Parser // NOTE: This is failing for //localhost:5000/api/book/29919/book-resources?file=OPS/images/tick1.jpg var importFile = match.Groups["Filename"].Value; - if (!importFile.Contains('?')) return importFile; + if (!importFile.Contains("?")) return importFile; } return null; } - - /// - /// If the name matches exactly Series (Volume digits) - /// - /// - /// - public static bool IsSeriesAndYear(string? name) - { - return !string.IsNullOrEmpty(name) && SeriesAndYearRegex.IsMatch(name); - } - - /// - /// Parse a Year from a Comic Series: Series Name (YEAR) - /// - /// Harley Quinn (2024) returns 2024 - /// - /// - public static string ParseYear(string? name) - { - if (string.IsNullOrEmpty(name)) return string.Empty; - var match = SeriesAndYearRegex.Match(name); - if (!match.Success) return string.Empty; - - return match.Groups["Year"].Value; - } - - public static string? RemoveExtensionIfSupported(string? filename) - { - if (string.IsNullOrEmpty(filename)) return filename; - - if (SupportedExtensionsRegex().IsMatch(filename)) - { - return SupportedExtensionsRegex().Replace(filename, string.Empty); - } - return filename; - } - - [GeneratedRegex(SupportedExtensions)] - private static partial Regex SupportedExtensionsRegex(); - [GeneratedRegex(@"\d-{1}\d")] - private static partial Regex NumberRangeRegex(); } diff --git a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs b/API/Services/Tasks/Scanner/Parser/ParserInfo.cs index 2a1540234..8cd81cf6d 100644 --- a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs +++ b/API/Services/Tasks/Scanner/Parser/ParserInfo.cs @@ -60,10 +60,6 @@ public class ParserInfo /// If the file contains no volume/chapter information or contains Special Keywords /// public bool IsSpecial { get; set; } - /// - /// If the file has a Special Marker explicitly, this will contain the index - /// - public int SpecialIndex { get; set; } = 0; /// /// Used for specials or books, stores what the UI should show. @@ -71,19 +67,13 @@ public class ParserInfo /// public string Title { get; set; } = string.Empty; - /// - /// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on . - /// Falls back to Parsed Chapter number - /// - public float IssueOrder { get; set; } - /// /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 /// /// public bool IsSpecialInfo() { - return (IsSpecial || (Volumes == Parser.LooseLeafVolume && Chapters == Parser.DefaultChapter)); + return (IsSpecial || (Volumes == Parser.DefaultVolume && Chapters == Parser.DefaultChapter)); } /// @@ -101,7 +91,7 @@ public class ParserInfo { if (info2 == null) return; Chapters = string.IsNullOrEmpty(Chapters) || Chapters == Parser.DefaultChapter ? info2.Chapters: Chapters; - Volumes = string.IsNullOrEmpty(Volumes) || Volumes == Parser.LooseLeafVolume ? info2.Volumes : Volumes; + Volumes = string.IsNullOrEmpty(Volumes) || Volumes == Parser.DefaultVolume ? info2.Volumes : Volumes; Edition = string.IsNullOrEmpty(Edition) ? info2.Edition : Edition; Title = string.IsNullOrEmpty(Title) ? info2.Title : Title; Series = string.IsNullOrEmpty(Series) ? info2.Series : Series; diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/API/Services/Tasks/Scanner/Parser/PdfParser.cs deleted file mode 100644 index 80bfa9a48..000000000 --- a/API/Services/Tasks/Scanner/Parser/PdfParser.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; - -namespace API.Services.Tasks.Scanner.Parser; - -public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) -{ - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) - { - var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); - var ret = new ParserInfo - { - Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = string.Empty, - ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type) - }; - - if (type == LibraryType.Book) - { - ret.Chapters = Parser.DefaultChapter; - } - - ret.Series = Parser.ParseSeries(fileName, type); - ret.Volumes = Parser.ParseVolume(fileName, type); - - if (ret.Series == string.Empty) - { - // Try to parse information out of each folder all the way to rootPath - ParseFromFallbackFolders(filePath, rootPath, type, ref ret); - } - - var edition = Parser.ParseEdition(fileName); - if (!string.IsNullOrEmpty(edition)) - { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); - ret.Edition = edition; - } - - var isSpecial = Parser.IsSpecial(fileName, type); - // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that - // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial) - { - ret.IsSpecial = true; - // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder - ParseFromFallbackFolders(filePath, rootPath, type, ref ret); - } - - // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Parser.HasSpecialMarker(fileName)) - { - ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; - - var tempRootPath = rootPath; - if (rootPath.EndsWith("Specials") || rootPath.EndsWith("Specials/")) - { - tempRootPath = rootPath.Replace("Specials", string.Empty).TrimEnd('/'); - } - - ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); - } - - if (enableMetadata) - { - // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); - - if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) - { - ret.Title = comicInfo.Title.Trim(); - } - } - - - if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book) - { - ret.IsSpecial = true; - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; - ParseFromFallbackFolders(filePath, rootPath, type, ref ret); - } - - if (type == LibraryType.Book && comicInfo != null) - { - // For books, fall back to the Title for Series. - if (!string.IsNullOrEmpty(comicInfo.Series)) - { - ret.Series = comicInfo.Series.Trim(); - } - else if (!string.IsNullOrEmpty(comicInfo.Title)) - { - ret.Series = comicInfo.Title.Trim(); - } - } - - if (string.IsNullOrEmpty(ret.Series)) - { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); - } - - // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) - { - ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); - } - - // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number - if (ret.IsSpecial) - { - ret.Volumes = $"{Parser.SpecialVolumeNumber}"; - } - - return string.IsNullOrEmpty(ret.Series) ? null : ret; - } - - /// - /// Only applicable for PDF files - /// - /// - /// - /// - public override bool IsApplicable(string filePath, LibraryType type) - { - return Parser.IsPdf(filePath); - } -} diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 307408adb..b42acafe7 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -10,26 +10,36 @@ using API.Data.Metadata; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; -using API.Services.Plus; using API.Services.Tasks.Metadata; using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; using Kavita.Common; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; + #nullable enable public interface IProcessSeries { - Task ProcessSeriesAsync(IList parsedInfos, Library library, int totalToProcess, bool forceUpdate = false); + /// + /// Do not allow this Prime to be invoked by multiple threads. It will break the DB. + /// + /// + Task Prime(); + Task ProcessSeriesAsync(IList parsedInfos, Library library, bool forceUpdate = false); + void EnqueuePostSeriesProcessTasks(int libraryId, int seriesId, bool forceUpdate = false); + + // These exists only for Unit testing + void UpdateSeriesMetadata(Series series, Library library); + void UpdateVolumes(Series series, IList parsedInfos, bool forceUpdate = false); + void UpdateChapters(Series series, Volume volume, IList parsedInfos, bool forceUpdate = false); + void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false); + void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo, bool forceUpdate = false); } /// @@ -46,15 +56,21 @@ public class ProcessSeries : IProcessSeries private readonly IFileService _fileService; private readonly IMetadataService _metadataService; private readonly IWordCountAnalyzerService _wordCountAnalyzerService; + private readonly ICollectionTagService _collectionTagService; private readonly IReadingListService _readingListService; - private readonly IExternalMetadataService _externalMetadataService; + private Dictionary _genres; + private IList _people; + private Dictionary _tags; + private Dictionary _collectionTags; + private readonly object _peopleLock = new object(); + private readonly object _genreLock = new object(); + private readonly object _tagLock = new object(); public ProcessSeries(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService, IFileService fileService, IMetadataService metadataService, IWordCountAnalyzerService wordCountAnalyzerService, - IReadingListService readingListService, - IExternalMetadataService externalMetadataService) + ICollectionTagService collectionTagService, IReadingListService readingListService) { _unitOfWork = unitOfWork; _logger = logger; @@ -65,12 +81,30 @@ public class ProcessSeries : IProcessSeries _fileService = fileService; _metadataService = metadataService; _wordCountAnalyzerService = wordCountAnalyzerService; + _collectionTagService = collectionTagService; _readingListService = readingListService; - _externalMetadataService = externalMetadataService; + + + _genres = new Dictionary(); + _people = new List(); + _tags = new Dictionary(); + _collectionTags = new Dictionary(); } + /// + /// Invoke this before processing any series, just once to prime all the needed data during a scan + /// + public async Task Prime() + { + _genres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToDictionary(t => t.NormalizedTitle); + _people = await _unitOfWork.PersonRepository.GetAllPeople(); + _tags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToDictionary(t => t.NormalizedTitle); + _collectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync(CollectionTagIncludes.SeriesMetadata)) + .ToDictionary(t => t.NormalizedTitle); - public async Task ProcessSeriesAsync(IList parsedInfos, Library library, int totalToProcess, bool forceUpdate = false) + } + + public async Task ProcessSeriesAsync(IList parsedInfos, Library library, bool forceUpdate = false) { if (!parsedInfos.Any()) return; @@ -78,23 +112,24 @@ public class ProcessSeries : IProcessSeries var scanWatch = Stopwatch.StartNew(); var seriesName = parsedInfos[0].Series; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Updated, seriesName, totalToProcess)); - _logger.LogInformation("[ScannerService] Beginning series update on {SeriesName}, Forced: {ForceUpdate}", seriesName, forceUpdate); + MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Updated, seriesName)); + _logger.LogInformation("[ScannerService] Beginning series update on {SeriesName}", seriesName); // Check if there is a Series var firstInfo = parsedInfos[0]; Series? series; try { - // There is an opportunity to allow duplicate series here. Like if One is in root/marvel/batman and another is root/dc/batman - // by changing to a ToList() and if multiple, doing a firstInfo.FirstFolder/RootFolder type check series = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(firstInfo.Series, firstInfo.LocalizedSeries, library.Id, firstInfo.Format); } catch (Exception ex) { - await ReportDuplicateSeriesLookup(library, firstInfo, ex); + _logger.LogError(ex, "There was an exception finding existing series for {SeriesName} with Localized name of {LocalizedName} for library {LibraryId}. This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan", firstInfo.Series, firstInfo.LocalizedSeries, library.Id); + await _eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent($"There was an exception finding existing series for {firstInfo.Series} with Localized name of {firstInfo.LocalizedSeries} for library {library.Id}", + "This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan.")); return; } @@ -111,12 +146,12 @@ public class ProcessSeries : IProcessSeries try { - _logger.LogInformation("[ScannerService] Processing series {SeriesName} with {Count} files", series.OriginalName, parsedInfos.Count); + _logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName); // parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort) var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo); - await UpdateVolumes(series, parsedInfos, forceUpdate); + UpdateVolumes(series, parsedInfos, forceUpdate); series.Pages = series.Volumes.Sum(v => v.Pages); series.NormalizedName = series.Name.ToNormalized(); @@ -126,17 +161,13 @@ public class ProcessSeries : IProcessSeries series.Format = firstParsedInfo.Format; } - var removePrefix = library.RemovePrefixForSortName; - var sortName = removePrefix ? BookSortTitlePrefixHelper.GetSortTitle(series.Name) : series.Name; - if (string.IsNullOrEmpty(series.SortName)) { - series.SortName = sortName; + series.SortName = series.Name; } - if (!series.SortNameLocked) { - series.SortName = sortName; + series.SortName = series.Name; if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort)) { series.SortName = firstParsedInfo.SeriesSort; @@ -151,7 +182,7 @@ public class ProcessSeries : IProcessSeries series.NormalizedLocalizedName = series.LocalizedName.ToNormalized(); } - await UpdateSeriesMetadata(series, library); + UpdateSeriesMetadata(series, library); // Update series FolderPath here await UpdateSeriesFolderPath(parsedInfos, library, series); @@ -164,22 +195,20 @@ public class ProcessSeries : IProcessSeries { await _unitOfWork.CommitAsync(); } - catch (DbUpdateConcurrencyException ex) - { - _logger.LogCritical(ex, - "[ScannerService] There was an issue writing to the database for series {SeriesName}", - series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series.OriginalName}", - ex.Message)); - return; - } catch (Exception ex) { await _unitOfWork.RollbackAsync(); _logger.LogCritical(ex, "[ScannerService] There was an issue writing to the database for series {SeriesName}", series.Name); + _logger.LogTrace("[ScannerService] Series Metadata Dump: {@Series}", series.Metadata); + _logger.LogTrace("[ScannerService] People Dump: {@People}", _people + .Select(p => + new {p.Id, p.Name, SeriesMetadataIds = + p.SeriesMetadatas?.Select(m => m.Id), + ChapterMetadataIds = + p.ChapterMetadatas?.Select(m => m.Id) + .ToList()})); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series.OriginalName}", @@ -187,27 +216,14 @@ public class ProcessSeries : IProcessSeries return; } - // Process reading list after commit as we need to commit per list - if (library.ManageReadingLists) - { - await _readingListService.CreateReadingListsFromSeries(series, library); - } - + await _readingListService.CreateReadingListsFromSeries(series, library); if (seriesAdded) { - // See if any recommendations can link up to the series and pre-fetch external metadata for the series - // BackgroundJob.Enqueue(() => - // _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type)); - await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded, MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false); } - else - { - await _unitOfWork.ExternalSeriesMetadataRepository.LinkRecommendationsToSeries(series); - } _logger.LogInformation("[ScannerService] Finished series update on {SeriesName} in {Milliseconds} ms", seriesName, scanWatch.ElapsedMilliseconds); } @@ -215,50 +231,18 @@ public class ProcessSeries : IProcessSeries catch (Exception ex) { _logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name); - return; } - if (seriesAdded) - { - await _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type); - } - await _metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false); - await _wordCountAnalyzerService.ScanSeries(series.LibraryId, series.Id, forceUpdate); - } - - private async Task ReportDuplicateSeriesLookup(Library library, ParserInfo firstInfo, Exception ex) - { - var seriesCollisions = await _unitOfWork.SeriesRepository.GetAllSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format); - - seriesCollisions = seriesCollisions.Where(collision => - collision.Name != firstInfo.Series || collision.LocalizedName != firstInfo.LocalizedSeries).ToList(); - - if (seriesCollisions.Count > 1) - { - var firstCollision = seriesCollisions[0]; - var secondCollision = seriesCollisions[1]; - - var tableRows = $"Name: {firstCollision.Name}Name: {secondCollision.Name}" + - $"Localized: {firstCollision.LocalizedName}Localized: {secondCollision.LocalizedName}" + - $"Filename: {Parser.Parser.NormalizePath(firstCollision.FolderPath)}Filename: {Parser.Parser.NormalizePath(secondCollision.FolderPath)}"; - - var htmlTable = $"{string.Join(string.Empty, tableRows)}
Series 1Series 2
"; - - _logger.LogError(ex, "[ScannerService] Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct, scan will abort", - firstInfo.Series, firstInfo.LocalizedSeries, library.Name); - - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent($"Library {library.Name} Series collision on {firstInfo.Series}", - htmlTable)); - } + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + await _metadataService.GenerateCoversForSeries(series, settings.EncodeMediaAs, settings.CoverImageSize); + EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id); } private async Task UpdateSeriesFolderPath(IEnumerable parsedInfos, Library library, Series series) { - var libraryFolders = library.Folders.Select(l => Parser.Parser.NormalizePath(l.Path)).ToList(); - var seriesFiles = parsedInfos.Select(f => Parser.Parser.NormalizePath(f.FullFilePath)).ToList(); - var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryFolders, seriesFiles); + var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(library.Folders.Select(l => l.Path), + parsedInfos.Select(f => f.FullFilePath).ToList()); if (seriesDirs.Keys.Count == 0) { _logger.LogCritical( @@ -272,33 +256,27 @@ public class ProcessSeries : IProcessSeries // Don't save FolderPath if it's a library Folder if (!library.Folders.Select(f => f.Path).Contains(seriesDirs.Keys.First())) { - // BUG: FolderPath can be a level higher than it needs to be. I'm not sure why it's like this, but I thought it should be one level lower. - // I think it's like this because higher level is checked or not checked. But i think we can do both series.FolderPath = Parser.Parser.NormalizePath(seriesDirs.Keys.First()); _logger.LogDebug("Updating {Series} FolderPath to {FolderPath}", series.Name, series.FolderPath); } } - - var lowestFolder = _directoryService.FindLowestDirectoriesFromFiles(libraryFolders, seriesFiles); - if (!string.IsNullOrEmpty(lowestFolder)) - { - series.LowestFolderPath = lowestFolder; - _logger.LogDebug("Updating {Series} LowestFolderPath to {FolderPath}", series.Name, series.LowestFolderPath); - } } + public void EnqueuePostSeriesProcessTasks(int libraryId, int seriesId, bool forceUpdate = false) + { + BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, seriesId, forceUpdate)); + } - private async Task UpdateSeriesMetadata(Series series, Library library) + public void UpdateSeriesMetadata(Series series, Library library) { series.Metadata ??= new SeriesMetadataBuilder().Build(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); var firstFile = firstChapter?.Files.FirstOrDefault(); if (firstFile == null) return; + if (Parser.Parser.IsPdf(firstFile.FilePath)) return; - var chapters = series.Volumes - .SelectMany(volume => volume.Chapters) - .ToList(); + var chapters = series.Volumes.SelectMany(volume => volume.Chapters).ToList(); // Update Metadata based on Chapter metadata if (!series.Metadata.ReleaseYearLocked) @@ -307,21 +285,36 @@ public class ProcessSeries : IProcessSeries } // Set the AgeRating as highest in all the comicInfos - if (!series.Metadata.AgeRatingLocked) - { - series.Metadata.AgeRating = chapters.Max(chapter => chapter.AgeRating); + if (!series.Metadata.AgeRatingLocked) series.Metadata.AgeRating = chapters.Max(chapter => chapter.AgeRating); - // Get the MetadataSettings and apply Age Rating Mappings here - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - var allTags = series.Metadata.Tags.Select(t => t.Title).Concat(series.Metadata.Genres.Select(g => g.Title)); - var updatedRating = ExternalMetadataService.DetermineAgeRating(allTags, metadataSettings.AgeRatingMappings); - if (updatedRating > series.Metadata.AgeRating) - { - series.Metadata.AgeRating = updatedRating; - } + // Count (aka expected total number of chapters or volumes from metadata) across all chapters + series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount); + // The actual number of count's defined across all chapter's metadata + series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); + + var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name)); + var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range)); + + if (maxChapter > series.Metadata.TotalCount && maxVolume <= series.Metadata.TotalCount) + { + series.Metadata.MaxCount = maxVolume; + } + else + { + series.Metadata.MaxCount = maxChapter; } - DeterminePublicationStatus(series, chapters); + if (!series.Metadata.PublicationStatusLocked) + { + series.Metadata.PublicationStatus = PublicationStatus.OnGoing; + if (series.Metadata.MaxCount >= series.Metadata.TotalCount && series.Metadata.TotalCount > 0) + { + series.Metadata.PublicationStatus = PublicationStatus.Completed; + } else if (series.Metadata.TotalCount > 0) + { + series.Metadata.PublicationStatus = PublicationStatus.Ended; + } + } if (!string.IsNullOrEmpty(firstChapter?.Summary) && !series.Metadata.SummaryLocked) { @@ -333,366 +326,199 @@ public class ProcessSeries : IProcessSeries series.Metadata.Language = firstChapter.Language; } - if (!string.IsNullOrEmpty(firstChapter?.SeriesGroup) && library.ManageCollections) { - await UpdateCollectionTags(series, firstChapter); - } - - #region PeopleAndTagsAndGenres - if (!series.Metadata.WriterLocked) + _logger.LogDebug("Collection tag(s) found for {SeriesName}, updating collections", series.Name); + foreach (var collection in firstChapter.SeriesGroup.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) { - var personSw = Stopwatch.StartNew(); - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Writer)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Writer)) + var normalizedName = Parser.Parser.Normalize(collection); + if (!_collectionTags.TryGetValue(normalizedName, out var tag)) { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Writer); + tag = _collectionTagService.CreateTag(collection); + _collectionTags.Add(normalizedName, tag); } - _logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Series: {File} for {Count} people", personSw.ElapsedMilliseconds, series.Name, chapterPeople.Count); + + _collectionTagService.AddTagToSeriesMetadata(tag, series.Metadata); } - - if (!series.Metadata.ColoristLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Colorist)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Colorist)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Colorist); - } - } - - if (!series.Metadata.PublisherLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Publisher)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Publisher)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Publisher); - } - } - - if (!series.Metadata.CoverArtistLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.CoverArtist)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.CoverArtist)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.CoverArtist); - } - } - - if (!series.Metadata.CharacterLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Character)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Character)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Character); - } - } - - if (!series.Metadata.EditorLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Editor)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Editor)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Editor); - } - } - - if (!series.Metadata.InkerLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Inker)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Inker)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Inker); - } - } - - if (!series.Metadata.ImprintLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Imprint)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Imprint)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Imprint); - } - } - - if (!series.Metadata.TeamLocked) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Team)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Team)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Team); - } - } - - if (!series.Metadata.LocationLocked && !series.Metadata.AllKavitaPlus(PersonRole.Location)) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Location)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Location)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Location); - } - } - - if (!series.Metadata.LettererLocked && !series.Metadata.AllKavitaPlus(PersonRole.Letterer)) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Letterer)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Location)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Letterer); - } - } - - if (!series.Metadata.PencillerLocked && !series.Metadata.AllKavitaPlus(PersonRole.Penciller)) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Penciller)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Penciller)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Penciller); - } - } - - if (!series.Metadata.TranslatorLocked && !series.Metadata.AllKavitaPlus(PersonRole.Translator)) - { - var chapterPeople = chapters.SelectMany(c => c.People.Where(p => p.Role == PersonRole.Translator)).ToList(); - if (ShouldUpdatePeopleForRole(series, chapterPeople, PersonRole.Translator)) - { - await UpdateSeriesMetadataPeople(series.Metadata, series.Metadata.People, chapterPeople, PersonRole.Translator); - } - } - - - if (!series.Metadata.TagsLocked) - { - var tags = chapters.SelectMany(c => c.Tags).ToList(); - UpdateSeriesMetadataTags(series.Metadata.Tags, tags); } if (!series.Metadata.GenresLocked) { var genres = chapters.SelectMany(c => c.Genres).ToList(); - UpdateSeriesMetadataGenres(series.Metadata.Genres, genres); + GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres.ToList(), genres, genre => + { + series.Metadata.Genres.Remove(genre); + }); } + + #region People + + // Handle People + foreach (var chapter in chapters) + { + if (!series.Metadata.WriterLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Writer)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.CoverArtistLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.CoverArtist)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.PublisherLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Publisher)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.CharacterLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Character)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.ColoristLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Colorist)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.EditorLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Editor)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.InkerLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Inker)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.LettererLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Letterer)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.PencillerLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Penciller)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.TranslatorLocked) + { + foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Translator)) + { + PersonHelper.AddPersonIfNotExists(series.Metadata.People, person); + } + } + + if (!series.Metadata.TagsLocked) + { + foreach (var tag in chapter.Tags) + { + TagHelper.AddTagIfNotExists(series.Metadata.Tags, tag); + } + } + + if (!series.Metadata.GenresLocked) + { + foreach (var genre in chapter.Genres) + { + GenreHelper.AddGenreIfNotExists(series.Metadata.Genres, genre); + } + } + } + // NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it + // I might be able to filter out people that are in locked fields? + var people = chapters.SelectMany(c => c.People).ToList(); + PersonHelper.KeepOnlySamePeopleBetweenLists(series.Metadata.People.ToList(), + people, person => + { + switch (person.Role) + { + case PersonRole.Writer: + if (!series.Metadata.WriterLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Penciller: + if (!series.Metadata.PencillerLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Inker: + if (!series.Metadata.InkerLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Colorist: + if (!series.Metadata.ColoristLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Letterer: + if (!series.Metadata.LettererLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.CoverArtist: + if (!series.Metadata.CoverArtistLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Editor: + if (!series.Metadata.EditorLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Publisher: + if (!series.Metadata.PublisherLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Character: + if (!series.Metadata.CharacterLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Translator: + if (!series.Metadata.TranslatorLocked) series.Metadata.People.Remove(person); + break; + case PersonRole.Other: + default: + series.Metadata.People.Remove(person); + break; + } + }); + #endregion } - /// - /// Ensure that we don't overwrite Person metadata when all metadata is coming from Kavita+ metadata match functionality - /// - /// - /// - /// - /// - private static bool ShouldUpdatePeopleForRole(Series series, List chapterPeople, PersonRole role) - { - if (chapterPeople.Count == 0) return false; - - // If metadata already has this role, but all entries are from KavitaPlus, we should retain them - if (series.Metadata.AnyOfRole(role)) - { - var existingPeople = series.Metadata.People.Where(p => p.Role == role); - - // If all existing people are KavitaPlus but new chapter people exist, we should still update - if (existingPeople.All(p => p.KavitaPlusConnection)) - { - return false; // Ensure we don't remove KavitaPlus people - } - - return true; // Default case: metadata exists, and it's okay to update - } - - return true; - } - - private async Task UpdateCollectionTags(Series series, Chapter firstChapter) - { - // Get the default admin to associate these tags to - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections); - if (defaultAdmin == null) return; - - _logger.LogInformation("Collection tag(s) found for {SeriesName}, updating collections", series.Name); - var sw = Stopwatch.StartNew(); - - foreach (var collection in firstChapter.SeriesGroup.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) - { - // Try to find an existing collection tag by its normalized name - var normalizedCollectionName = collection.ToNormalized(); - var collectionTag = defaultAdmin.Collections.FirstOrDefault(c => c.NormalizedTitle == normalizedCollectionName); - - // If the collection tag does not exist, create a new one - if (collectionTag == null) - { - _logger.LogDebug("Creating new collection tag for {Tag}", collection); - - collectionTag = new AppUserCollectionBuilder(collection).Build(); - defaultAdmin.Collections.Add(collectionTag); - - _unitOfWork.UserRepository.Update(defaultAdmin); - - await _unitOfWork.CommitAsync(); - } - - // Check if the Series is already associated with this collection - if (collectionTag.Items.Any(s => s.MatchesSeriesByName(series.NormalizedName, series.NormalizedLocalizedName))) - { - continue; - } - - // Add the series to the collection tag - collectionTag.Items.Add(series); - - // Update the collection age rating - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collectionTag); - } - - _logger.LogTrace("[TIME] Kavita took {Time} ms to process collections on Series: {Name}", sw.ElapsedMilliseconds, series.Name); - } - - - private static void UpdateSeriesMetadataTags(ICollection metadataTags, IList chapterTags) - { - // Create a HashSet of normalized titles for faster lookups - var chapterTagTitles = new HashSet(chapterTags.Select(t => t.NormalizedTitle)); - - // Remove any tags from metadataTags that are not part of chapterTags - var tagsToRemove = metadataTags - .Where(mt => !chapterTagTitles.Contains(mt.NormalizedTitle)) - .ToList(); - - if (tagsToRemove.Count > 0) - { - foreach (var tagToRemove in tagsToRemove) - { - metadataTags.Remove(tagToRemove); - } - } - - // Create a HashSet of metadataTags normalized titles for faster lookup - var metadataTagTitles = new HashSet(metadataTags.Select(mt => mt.NormalizedTitle)); - - // Add any tags from chapterTags that do not already exist in metadataTags - foreach (var tag in chapterTags) - { - if (!metadataTagTitles.Contains(tag.NormalizedTitle)) - { - metadataTags.Add(tag); - } - } - } - - private static void UpdateSeriesMetadataGenres(ICollection metadataGenres, IList chapterGenres) - { - // Create a HashSet of normalized titles for chapterGenres for fast lookup - var chapterGenreTitles = new HashSet(chapterGenres.Select(g => g.NormalizedTitle)); - - // Remove any genres from metadataGenres that are not present in chapterGenres - var genresToRemove = metadataGenres - .Where(mg => !chapterGenreTitles.Contains(mg.NormalizedTitle)) - .ToList(); - - foreach (var genreToRemove in genresToRemove) - { - metadataGenres.Remove(genreToRemove); - } - - // Create a HashSet of metadataGenres normalized titles for fast lookup - var metadataGenreTitles = new HashSet(metadataGenres.Select(mg => mg.NormalizedTitle)); - - // Add any genres from chapterGenres that are not already in metadataGenres - foreach (var genre in chapterGenres) - { - if (!metadataGenreTitles.Contains(genre.NormalizedTitle)) - { - metadataGenres.Add(genre); - } - } - } - - - - private async Task UpdateSeriesMetadataPeople(SeriesMetadata metadata, ICollection metadataPeople, - IEnumerable chapterPeople, PersonRole role) - { - await PersonHelper.UpdateSeriesMetadataPeopleAsync(metadata, metadataPeople, chapterPeople, role, _unitOfWork); - } - - private void DeterminePublicationStatus(Series series, List chapters) - { - try - { - // Count (aka expected total number of chapters or volumes from metadata) across all chapters - series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount); - // The actual number of count's defined across all chapter's metadata - series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); - - var nonSpecialVolumes = series.Volumes - .Where(v => v.MaxNumber.IsNot(Parser.Parser.SpecialVolumeNumber)) - .ToList(); - - var maxVolume = (int)(nonSpecialVolumes.Any() ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); - var maxChapter = (int)chapters.Max(c => c.MaxNumber); - - // Single books usually don't have a number in their Range (filename) - if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) - { - series.Metadata.MaxCount = 1; - } - else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial) - { - // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete - series.Metadata.MaxCount = series.Metadata.TotalCount; - } - else if ((maxChapter == Parser.Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && - maxVolume <= series.Metadata.TotalCount) - { - series.Metadata.MaxCount = maxVolume; - } - else if (maxVolume == series.Metadata.TotalCount) - { - series.Metadata.MaxCount = maxVolume; - } - else - { - series.Metadata.MaxCount = maxChapter; - } - - if (!series.Metadata.PublicationStatusLocked) - { - series.Metadata.PublicationStatus = PublicationStatus.OnGoing; - if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) - { - series.Metadata.PublicationStatus = PublicationStatus.Completed; - } - else if (series.Metadata.TotalCount > 0 && series.Metadata.MaxCount > 0) - { - series.Metadata.PublicationStatus = PublicationStatus.Ended; - } - } - } - catch (Exception ex) - { - _logger.LogCritical(ex, "There was an issue determining Publication Status"); - series.Metadata.PublicationStatus = PublicationStatus.OnGoing; - } - } - - private async Task UpdateVolumes(Series series, IList parsedInfos, bool forceUpdate = false) + public void UpdateVolumes(Series series, IList parsedInfos, bool forceUpdate = false) { + var startingVolumeCount = series.Volumes.Count; // Add new volumes and update chapters per volume var distinctVolumes = parsedInfos.DistinctVolumes(); + _logger.LogDebug("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name); foreach (var volumeNumber in distinctVolumes) { Volume? volume; try { - // With the Name change to be formatted, Name no longer working because Name returns "1" and volumeNumber is "1.0", so we use LookupName as the original - volume = series.Volumes.SingleOrDefault(s => s.LookupName == volumeNumber); + volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber); } catch (Exception ex) { - // TODO: Push this to UI in some way if (!ex.Message.Equals("Sequence contains more than one matching element")) throw; - _logger.LogCritical(ex, "[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name); + _logger.LogCritical("[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name); throw new KavitaException( $"Kavita found corrupted volume entries on {series.Name}. Please delete the series from Kavita via UI and rescan"); } @@ -704,49 +530,59 @@ public class ProcessSeries : IProcessSeries series.Volumes.Add(volume); } - volume.LookupName = volumeNumber; - volume.Name = volume.GetNumberTitle(); + volume.Name = volumeNumber; + _logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); - - await UpdateChapters(series, volume, infos, forceUpdate); + UpdateChapters(series, volume, infos, forceUpdate); volume.Pages = volume.Chapters.Sum(c => c.Pages); + + // Update all the metadata on the Chapters + foreach (var chapter in volume.Chapters) + { + var firstFile = chapter.Files.MinBy(x => x.Chapter); + if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) continue; + try + { + var firstChapterInfo = infos.SingleOrDefault(i => i.FullFilePath.Equals(firstFile.FilePath)); + UpdateChapterFromComicInfo(chapter, firstChapterInfo?.ComicInfo, forceUpdate); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was some issue when updating chapter's metadata"); + } + } } // Remove existing volumes that aren't in parsedInfos - RemoveVolumes(series, parsedInfos); - } - - private void RemoveVolumes(Series series, IList parsedInfos) - { - - var nonDeletedVolumes = series.Volumes - .Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.LookupName)) - .ToList(); - if (series.Volumes.Count == nonDeletedVolumes.Count) return; - - - _logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", - (series.Volumes.Count - nonDeletedVolumes.Count), series.Name); - var deletedVolumes = series.Volumes.Except(nonDeletedVolumes); - foreach (var volume in deletedVolumes) + var nonDeletedVolumes = series.Volumes.Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.Name)).ToList(); + if (series.Volumes.Count != nonDeletedVolumes.Count) { - var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? string.Empty; - if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file)) + _logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", + (series.Volumes.Count - nonDeletedVolumes.Count), series.Name); + var deletedVolumes = series.Volumes.Except(nonDeletedVolumes); + foreach (var volume in deletedVolumes) { - // This can happen when file is renamed and volume is removed - _logger.LogInformation( - "[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk (usually volume marker removed) File: {File}", - file); + var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? string.Empty; + if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file)) + { + _logger.LogInformation( + "[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}", + file); + } + + _logger.LogDebug("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file); } - _logger.LogDebug("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file); + series.Volumes = nonDeletedVolumes; } - series.Volumes = nonDeletedVolumes; + // DO I need this anymore? + _logger.LogDebug("[ScannerService] Updated {SeriesName} volumes from count of {StartingVolumeCount} to {VolumeCount}", + series.Name, startingVolumeCount, series.Volumes.Count); } - private async Task UpdateChapters(Series series, Volume volume, IList parsedInfos, bool forceUpdate = false) + public void UpdateChapters(Series series, Volume volume, IList parsedInfos, bool forceUpdate = false) { // Add new chapters foreach (var info in parsedInfos) @@ -777,115 +613,47 @@ public class ProcessSeries : IProcessSeries chapter.UpdateFrom(info); } - + if (chapter == null) continue; // Add files + var specialTreatment = info.IsSpecialInfo(); AddOrUpdateFileForChapter(chapter, info, forceUpdate); - chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); - chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters); - chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters); - chapter.Range = chapter.GetNumberTitle(); - - if (!chapter.SortOrderLocked) - { - chapter.SortOrder = info.IssueOrder; - } - - if (float.TryParse(chapter.Title, CultureInfo.InvariantCulture, out _)) - { - // If we have float based chapters, first scan can have the chapter formatted as Chapter 0.2 - .2 as the title is wrong. - chapter.Title = chapter.GetNumberTitle(); - } - - try - { - await UpdateChapterFromComicInfo(chapter, info.ComicInfo, forceUpdate); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was some issue when updating chapter's metadata"); - } - + chapter.Range = specialTreatment ? info.Filename : info.Chapters; } - RemoveChapters(volume, parsedInfos); - } - - private void RemoveChapters(Volume volume, IList parsedInfos) - { - // Chapters to remove after enumeration - var chaptersToRemove = new List(); - - var existingChapters = volume.Chapters; - - // Extract the directories (without filenames) from parserInfos - var parsedDirectories = parsedInfos - .Select(p => Path.GetDirectoryName(p.FullFilePath)) - .Distinct() - .ToList(); + // Remove chapters that aren't in parsedInfos or have no files linked + var existingChapters = volume.Chapters.ToList(); foreach (var existingChapter in existingChapters) { - var chapterFileDirectories = existingChapter.Files - .Select(f => Path.GetDirectoryName(f.FilePath)) - .Distinct() - .ToList(); - - var hasMatchingDirectory = chapterFileDirectories.Exists(dir => parsedDirectories.Contains(dir)); - - if (hasMatchingDirectory) + if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter)) { - existingChapter.Files = existingChapter.Files - .Where(f => parsedInfos.Any(p => Parser.Parser.NormalizePath(p.FullFilePath) == Parser.Parser.NormalizePath(f.FilePath))) - .OrderByNatural(f => f.FilePath) - .ToList(); - - existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages); - - if (existingChapter.Files.Count != 0) continue; - - _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", - existingChapter.Range, volume.Name, parsedInfos[0].Series); - chaptersToRemove.Add(existingChapter); // Mark chapter for removal + _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series); + volume.Chapters.Remove(existingChapter); } else { - var filesExist = existingChapter.Files.Any(f => File.Exists(f.FilePath)); - if (filesExist) continue; - - _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName} as no files exist", - existingChapter.Range, volume.Name, parsedInfos[0].Series); - chaptersToRemove.Add(existingChapter); // Mark chapter for removal + // Ensure we remove any files that no longer exist AND order + existingChapter.Files = existingChapter.Files + .Where(f => parsedInfos.Any(p => p.FullFilePath == f.FilePath)) + .OrderByNatural(f => f.FilePath).ToList(); + existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages); } } - - // Remove chapters after the loop to avoid modifying the collection during enumeration - foreach (var chapter in chaptersToRemove) - { - volume.Chapters.Remove(chapter); - } } - - private void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false) + public void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false) { chapter.Files ??= new List(); var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath); var fileInfo = _directoryService.FileSystem.FileInfo.New(info.FullFilePath); if (existingFile != null) { - // TODO: I wonder if we can simplify this force check. existingFile.Format = info.Format; - if (!forceUpdate && !_fileService.HasFileBeenModifiedSince(existingFile.FilePath, existingFile.LastModified) && existingFile.Pages != 0) return; - existingFile.Pages = _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format); existingFile.Extension = fileInfo.Extension.ToLowerInvariant(); - existingFile.FileName = Parser.Parser.RemoveExtensionIfSupported(existingFile.FilePath); - existingFile.FilePath = Parser.Parser.NormalizePath(existingFile.FilePath); existingFile.Bytes = fileInfo.Length; - existingFile.KoreaderHash = KoreaderHelper.HashContents(existingFile.FilePath); - // We skip updating DB here with last modified time so that metadata refresh can do it } else @@ -894,36 +662,33 @@ public class ProcessSeries : IProcessSeries var file = new MangaFileBuilder(info.FullFilePath, info.Format, _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format)) .WithExtension(fileInfo.Extension) .WithBytes(fileInfo.Length) - .WithHash() .Build(); chapter.Files.Add(file); } } - private async Task UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo, bool forceUpdate = false) + public void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo, bool forceUpdate = false) { if (comicInfo == null) return; var firstFile = chapter.Files.MinBy(x => x.Chapter); if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; - var sw = Stopwatch.StartNew(); - if (!chapter.AgeRatingLocked) - { - chapter.AgeRating = ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating); - } + _logger.LogTrace("[ScannerService] Read ComicInfo for {File}", firstFile.FilePath); - if (!chapter.TitleNameLocked && !string.IsNullOrEmpty(comicInfo.Title)) + chapter.AgeRating = ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating); + + if (!string.IsNullOrEmpty(comicInfo.Title)) { chapter.TitleName = comicInfo.Title.Trim(); } - if (!chapter.SummaryLocked && !string.IsNullOrEmpty(comicInfo.Summary)) + if (!string.IsNullOrEmpty(comicInfo.Summary)) { chapter.Summary = comicInfo.Summary; } - if (!chapter.LanguageLocked && !string.IsNullOrEmpty(comicInfo.LanguageISO)) + if (!string.IsNullOrEmpty(comicInfo.LanguageISO)) { chapter.Language = comicInfo.LanguageISO; } @@ -961,13 +726,13 @@ public class ProcessSeries : IProcessSeries if (!string.IsNullOrEmpty(comicInfo.Web)) { chapter.WebLinks = string.Join(",", comicInfo.Web - .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Split(",") + .Where(s => !string.IsNullOrEmpty(s)) + .Select(s => s.Trim()) ); - - // TODO: For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL) } - if (!chapter.ISBNLocked && !string.IsNullOrEmpty(comicInfo.Isbn)) + if (!string.IsNullOrEmpty(comicInfo.Isbn)) { chapter.ISBN = comicInfo.Isbn; } @@ -980,144 +745,180 @@ public class ProcessSeries : IProcessSeries // This needs to check against both Number and Volume to calculate Count chapter.Count = comicInfo.CalculatedCount(); + void AddPerson(Person person) + { + PersonHelper.AddPersonIfNotExists(chapter.People, person); + } - if (!chapter.ReleaseDateLocked && comicInfo.Year > 0) + void AddGenre(Genre genre, bool newTag) + { + chapter.Genres.Add(genre); + } + + void AddTag(Tag tag, bool added) + { + chapter.Tags.Add(tag); + } + + + if (comicInfo.Year > 0) { var day = Math.Max(comicInfo.Day, 1); var month = Math.Max(comicInfo.Month, 1); chapter.ReleaseDate = new DateTime(comicInfo.Year, month, day); } - if (!chapter.IsPersonRoleLocked(PersonRole.Colorist)) - { - var people = TagHelper.GetTagValues(comicInfo.Colorist); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Colorist); - } + var people = GetTagValues(comicInfo.Colorist); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Colorist); + UpdatePeople(people, PersonRole.Colorist, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Character)) - { - var people = TagHelper.GetTagValues(comicInfo.Characters); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Character); - } + people = GetTagValues(comicInfo.Characters); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Character); + UpdatePeople(people, PersonRole.Character, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Translator)) - { - var people = TagHelper.GetTagValues(comicInfo.Translator); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Translator); - } + people = GetTagValues(comicInfo.Translator); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Translator); + UpdatePeople(people, PersonRole.Translator, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Writer)) - { - var personSw = Stopwatch.StartNew(); - var people = TagHelper.GetTagValues(comicInfo.Writer); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Writer); - _logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Chapter: {File} for {Count} people", personSw.ElapsedMilliseconds, chapter.Files.First().FileName, people.Count); - } - if (!chapter.IsPersonRoleLocked(PersonRole.Editor)) - { - var people = TagHelper.GetTagValues(comicInfo.Editor); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Editor); - } + people = GetTagValues(comicInfo.Writer); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Writer); + UpdatePeople(people, PersonRole.Writer, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Inker)) - { - var people = TagHelper.GetTagValues(comicInfo.Inker); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Inker); - } + people = GetTagValues(comicInfo.Editor); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Editor); + UpdatePeople(people, PersonRole.Editor, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Letterer)) - { - var people = TagHelper.GetTagValues(comicInfo.Letterer); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Letterer); - } + people = GetTagValues(comicInfo.Inker); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Inker); + UpdatePeople(people, PersonRole.Inker, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Penciller)) - { - var people = TagHelper.GetTagValues(comicInfo.Penciller); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Penciller); - } + people = GetTagValues(comicInfo.Letterer); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Letterer); + UpdatePeople(people, PersonRole.Letterer, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.CoverArtist)) - { - var people = TagHelper.GetTagValues(comicInfo.CoverArtist); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.CoverArtist); - } + people = GetTagValues(comicInfo.Penciller); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Penciller); + UpdatePeople(people, PersonRole.Penciller, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Publisher)) - { - var people = TagHelper.GetTagValues(comicInfo.Publisher); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Publisher); - } + people = GetTagValues(comicInfo.CoverArtist); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.CoverArtist); + UpdatePeople(people, PersonRole.CoverArtist, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Imprint)) - { - var people = TagHelper.GetTagValues(comicInfo.Imprint); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Imprint); - } + people = GetTagValues(comicInfo.Publisher); + PersonHelper.RemovePeople(chapter.People, people, PersonRole.Publisher); + UpdatePeople(people, PersonRole.Publisher, AddPerson); - if (!chapter.IsPersonRoleLocked(PersonRole.Team)) - { - var people = TagHelper.GetTagValues(comicInfo.Teams); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Team); - } + var genres = GetTagValues(comicInfo.Genre); + GenreHelper.KeepOnlySameGenreBetweenLists(chapter.Genres, + genres.Select(g => new GenreBuilder(g).Build()).ToList()); + UpdateGenre(genres, AddGenre); - if (!chapter.IsPersonRoleLocked(PersonRole.Location)) - { - var people = TagHelper.GetTagValues(comicInfo.Locations); - await UpdateChapterPeopleAsync(chapter, people, PersonRole.Location); - } - - if (!chapter.GenresLocked) - { - var genres = TagHelper.GetTagValues(comicInfo.Genre); - await UpdateChapterGenres(chapter, genres); - } - - if (!chapter.TagsLocked) - { - var tags = TagHelper.GetTagValues(comicInfo.Tags); - await UpdateChapterTags(chapter, tags); - } - - _logger.LogTrace("[TIME] Kavita took {Time} ms to create/update Chapter: {File}", sw.ElapsedMilliseconds, chapter.Files.First().FileName); + var tags = GetTagValues(comicInfo.Tags); + TagHelper.KeepOnlySameTagBetweenLists(chapter.Tags, tags.Select(t => new TagBuilder(t).Build()).ToList()); + UpdateTag(tags, AddTag); } - private async Task UpdateChapterGenres(Chapter chapter, IEnumerable genreNames) + private static IList GetTagValues(string comicInfoTagSeparatedByComma) { - try + // TODO: Move this to an extension and test it + if (string.IsNullOrEmpty(comicInfoTagSeparatedByComma)) { - await GenreHelper.UpdateChapterGenres(chapter, genreNames, _unitOfWork); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error updating the chapter genres"); + return ImmutableList.Empty; } + + return comicInfoTagSeparatedByComma.Split(",") + .Select(s => s.Trim()) + .DistinctBy(Parser.Parser.Normalize) + .ToList(); } - - private async Task UpdateChapterTags(Chapter chapter, IEnumerable tagNames) + /// + /// Given a list of all existing people, this will check the new names and roles and if it doesn't exist in allPeople, will create and + /// add an entry. For each person in name, the callback will be executed. + /// + /// This does not remove people if an empty list is passed into names + /// This is used to add new people to a list without worrying about duplicating rows in the DB + /// + /// + /// + private void UpdatePeople(IEnumerable names, PersonRole role, Action action) { - try + lock (_peopleLock) { - await TagHelper.UpdateChapterTags(chapter, tagNames, _unitOfWork); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error updating the chapter tags"); + var allPeopleTypeRole = _people.Where(p => p.Role == role).ToList(); + + foreach (var name in names) + { + var normalizedName = name.ToNormalized(); + var person = allPeopleTypeRole.Find(p => + p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); + + if (person == null) + { + person = new PersonBuilder(name, role).Build(); + _people.Add(person); + } + action(person); + } } } - private async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role) + /// + /// + /// + /// + /// Executes for each tag + private void UpdateGenre(IEnumerable names, Action action) { - try + foreach (var name in names) { - await PersonHelper.UpdateChapterPeopleAsync(chapter, people, role, _unitOfWork); - } - catch (Exception ex) - { - _logger.LogError(ex, "[ScannerService] There was an issue adding/updating a person"); + var normalizedName = name.ToNormalized(); + if (string.IsNullOrEmpty(normalizedName)) continue; + + _genres.TryGetValue(normalizedName, out var genre); + var newTag = genre == null; + if (newTag) + { + genre = new GenreBuilder(name).Build(); + lock (_genreLock) + { + _genres.Add(normalizedName, genre); + _unitOfWork.GenreRepository.Attach(genre); + } + } + + action(genre!, newTag); } } + + /// + /// + /// + /// + /// Callback for every item. Will give said item back and a bool if item was added + private void UpdateTag(IEnumerable names, Action action) + { + foreach (var name in names) + { + if (string.IsNullOrEmpty(name.Trim())) continue; + + var normalizedName = name.ToNormalized(); + _tags.TryGetValue(normalizedName, out var tag); + + var added = tag == null; + if (tag == null) + { + tag = new TagBuilder(name).Build(); + lock (_tagLock) + { + _tags.Add(normalizedName, tag); + } + } + + action(tag, added); + } + } + } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index cb5f4302f..e42ba42cc 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; @@ -12,7 +11,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers; -using API.Helpers.Builders; using API.Services.Tasks.Metadata; using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; @@ -21,8 +19,6 @@ using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -#nullable enable - public interface IScannerService { /// @@ -34,7 +30,7 @@ public interface IScannerService [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true); + Task ScanLibrary(int libraryId, bool forceUpdate = false); [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] @@ -46,7 +42,7 @@ public interface IScannerService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true); - Task ScanFolder(string folder, string originalPath); + Task ScanFolder(string folder); Task AnalyzeFiles(); } @@ -77,7 +73,6 @@ public enum ScanCancelReason public class ScannerService : IScannerService { public const string Name = "ScannerService"; - private const int Timeout = 60 * 60 * 60; // 2.5 days private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IMetadataService _metadataService; @@ -137,47 +132,39 @@ public class ScannerService : IScannerService /// Given a generic folder path, will invoke a Series scan or Library scan. /// /// This will Schedule the job to run 1 minute in the future to allow for any close-by duplicate requests to be dropped - /// Normalized folder - /// If invoked from LibraryWatcher, this maybe a nested folder and can allow for optimization - public async Task ScanFolder(string folder, string originalPath) + /// + public async Task ScanFolder(string folder) { Series? series = null; try { - series = await _unitOfWork.SeriesRepository.GetSeriesThatContainsLowestFolderPath(originalPath, - SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(originalPath, SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); + series = await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); } catch (InvalidOperationException ex) { if (ex.Message.Equals("Sequence contains more than one element.")) { - _logger.LogCritical(ex, "[ScannerService] Multiple series map to this folder or folder is at library root. Library scan will be used for ScanFolder"); + _logger.LogCritical("[ScannerService] Multiple series map to this folder. Library scan will be used for ScanFolder"); } } - - if (series != null) + if (series != null && series.Library.Type != LibraryType.Book) { if (TaskScheduler.HasScanTaskRunningForSeries(series.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); + _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); return; } - - _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder}, Series matched to folder and ScanSeries enqueued for 1 minute", folder); BackgroundJob.Schedule(() => ScanSeries(series.Id, true), TimeSpan.FromMinutes(1)); return; } - // This is basically rework of what's already done in Library Watcher but is needed if invoked via API var parentDirectory = _directoryService.GetParentDirectoryName(folder); if (string.IsNullOrEmpty(parentDirectory)) return; var libraries = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()).ToList(); var libraryFolders = libraries.SelectMany(l => l.Folders); - var libraryFolder = libraryFolders.Select(Parser.NormalizePath).FirstOrDefault(f => f.Contains(parentDirectory)); + var libraryFolder = libraryFolders.Select(Scanner.Parser.Parser.NormalizePath).FirstOrDefault(f => f.Contains(parentDirectory)); if (string.IsNullOrEmpty(libraryFolder)) return; var library = libraries.Find(l => l.Folders.Select(Parser.NormalizePath).Contains(libraryFolder)); @@ -186,10 +173,10 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasScanTaskRunningForLibrary(library.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); + _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); return; } - BackgroundJob.Schedule(() => ScanLibrary(library.Id, false, true), TimeSpan.FromMinutes(1)); + BackgroundJob.Schedule(() => ScanLibrary(library.Id, false), TimeSpan.FromMinutes(1)); } } @@ -199,42 +186,28 @@ public class ScannerService : IScannerService /// /// Not Used. Scan series will always force [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(Timeout)] - [AutomaticRetry(Attempts = 200, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true) { - if (TaskScheduler.HasAlreadyEnqueuedTask(Name, "ScanSeries", [seriesId, bypassFolderOptimizationChecks], TaskScheduler.ScanQueue)) - { - _logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Dropping request"); - return; - } - var sw = Stopwatch.StartNew(); - + var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update - - var existingChapterIdsToClean = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); - - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); + var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders); if (library == null) return; - var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) { - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false, false)); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); return; } - // TODO: We need to refactor this to handle the path changes better - var folderPath = series.LowestFolderPath ?? series.FolderPath; + var folderPath = series.FolderPath; if (string.IsNullOrEmpty(folderPath) || !_directoryService.Exists(folderPath)) { // We don't care if it's multiple due to new scan loop enforcing all in one root directory - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); - var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, - files.Select(f => f.FilePath).ToList()); + var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); if (seriesDirs.Keys.Count == 0) { _logger.LogCritical("Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)"); @@ -251,6 +224,7 @@ public class ScannerService : IScannerService await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); return; } + } if (string.IsNullOrEmpty(folderPath)) @@ -260,24 +234,29 @@ public class ScannerService : IScannerService return; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name, 1)); + // If the series path doesn't exist anymore, it was either moved or renamed. We need to essentially delete it + var parsedSeries = new Dictionary>(); + + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name)); + + await _processSeries.Prime(); _logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); - var (scanElapsedTime, parsedSeries) = await ScanFiles(library, [folderPath], - false, true); + var scanElapsedTime = await ScanFiles(library, new []{ folderPath }, false, TrackFiles, true); + _logger.LogInformation("ScanFiles for {Series} took {Time}", series.Name, scanElapsedTime); + + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); + - _logger.LogInformation("ScanFiles for {Series} took {Time} milliseconds", series.Name, scanElapsedTime); // Remove any parsedSeries keys that don't belong to our series. This can occur when users store 2 series in the same folder RemoveParsedInfosNotForSeries(parsedSeries, series); - // If nothing was found, first validate any of the files still exist. If they don't then we have a deletion and can skip the rest of the logic flow - if (parsedSeries.Count == 0) - { + // If nothing was found, first validate any of the files still exist. If they don't then we have a deletion and can skip the rest of the logic flow + if (parsedSeries.Count == 0) + { var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); - if (!string.IsNullOrEmpty(series.FolderPath) && - !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath))) + if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath))) { try { @@ -302,57 +281,44 @@ public class ScannerService : IScannerService await _unitOfWork.RollbackAsync(); return; } - } + // At this point, parsedSeries will have at least one key and we can perform the update. If it still doesn't, just return and don't do anything + if (parsedSeries.Count == 0) return; + } - // At this point, parsedSeries will have at least one key then we can perform the update. If it still doesn't, just return and don't do anything - // Don't allow any processing on files that aren't part of this series - var toProcess = parsedSeries.Keys.Where(key => - key.NormalizedName.Equals(series.NormalizedName) || - key.NormalizedName.Equals(series.OriginalName?.ToNormalized())) - .ToList(); - var seriesLeftToProcess = toProcess.Count; - foreach (var pSeries in toProcess) - { - // Process Series - var seriesProcessStopWatch = Stopwatch.StartNew(); - await _processSeries.ProcessSeriesAsync(parsedSeries[pSeries], library, seriesLeftToProcess, bypassFolderOptimizationChecks); - _logger.LogTrace("[TIME] Kavita took {Time} ms to process {SeriesName}", seriesProcessStopWatch.ElapsedMilliseconds, parsedSeries[pSeries][0].Series); - seriesLeftToProcess--; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name, 0)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); // Tell UI that this series is done await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(library.Id, seriesId, series.Name)); await _metadataService.RemoveAbandonedMetadataKeys(); + BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(series.LibraryId, seriesId, false)); + BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false)); + BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); + BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); + return; - BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(existingChapterIdsToClean)); - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); - } - - private static Dictionary> TrackFoundSeriesAndFiles(IList seenSeries) - { - // Why does this only grab things that have changed? - var parsedSeries = new Dictionary>(); - foreach (var series in seenSeries.Where(s => s.ParsedInfos.Count > 0)) // && s.HasChanged + async Task TrackFiles(Tuple> parsedInfo) { - var parsedFiles = series.ParsedInfos; - series.ParsedSeries.HasChanged = series.HasChanged; + var parsedFiles = parsedInfo.Item2; + if (parsedFiles.Count == 0) return; - if (series.HasChanged) + var foundParsedSeries = new ParsedSeries() { - parsedSeries.Add(series.ParsedSeries, parsedFiles); - } - else + Name = parsedFiles[0].Series, + NormalizedName = parsedFiles[0].Series.ToNormalized(), + Format = parsedFiles[0].Format + }; + + // For Scan Series, we need to filter out anything that isn't our Series + if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(series.OriginalName?.ToNormalized())) { - parsedSeries.Add(series.ParsedSeries, []); + return; } + + await _processSeries.ProcessSeriesAsync(parsedFiles, library, bypassFolderOptimizationChecks); + parsedSeries.Add(foundParsedSeries, parsedFiles); } - - return parsedSeries; } private async Task ShouldScanSeries(int seriesId, Library library, IList libraryPaths, Series series, bool bypassFolderChecks = false) @@ -444,7 +410,7 @@ public class ScannerService : IScannerService // Check if any of the folder roots are not available (ie disconnected from network, etc) and fail if any of them are if (folders.Any(f => !_directoryService.IsDriveMounted(f))) { - _logger.LogCritical("[ScannerService] Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); + _logger.LogCritical("Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted", @@ -458,14 +424,14 @@ public class ScannerService : IScannerService if (folders.Any(f => _directoryService.IsDirectoryEmpty(f))) { // That way logging and UI informing is all in one place with full context - _logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + + _logger.LogError("Some of the root folders for the library are empty. " + "Either your mount has been disconnected or you are trying to delete all series in the library. " + - "Scan has been aborted. " + + "Scan has be aborted. " + "Check that your mount is connected or change the library's root folder and rescan"); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", "Either your mount has been disconnected or you are trying to delete all series in the library. " + - "Scan has been aborted. " + + "Scan has be aborted. " + "Check that your mount is connected or change the library's root folder and rescan")); return false; @@ -475,25 +441,16 @@ public class ScannerService : IScannerService } [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(Timeout)] + [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanLibraries(bool forceUpdate = false) { - _logger.LogInformation("[ScannerService] Starting Scan of All Libraries, Forced: {Forced}", forceUpdate); + _logger.LogInformation("Starting Scan of All Libraries"); foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync()) { - // BUG: This will trigger the first N libraries to scan over and over if there is always an interruption later in the chain - if (TaskScheduler.HasScanTaskRunningForLibrary(lib.Id)) - { - // We don't need to send SignalR event as this is a background job that user doesn't need insight into - _logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running for {LibraryName}. Rescheduling for 4 hours", lib.Name); - await Task.Delay(TimeSpan.FromHours(4)); - } - - await ScanLibrary(lib.Id, forceUpdate, true); + await ScanLibrary(lib.Id, forceUpdate); } - - _logger.LogInformation("[ScannerService] Scan of All Libraries Finished"); + _logger.LogInformation("Scan of All Libraries Finished"); } @@ -504,16 +461,13 @@ public class ScannerService : IScannerService ///
/// /// Defaults to false - /// Defaults to true. Is this a standalone invocation or is it in a loop? [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(Timeout)] + [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true) + public async Task ScanLibrary(int libraryId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, - LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); - + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders); var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList(); if (!await CheckMounts(library.Name, libraryFolderPaths)) return; @@ -521,166 +475,35 @@ public class ScannerService : IScannerService // Validations are done, now we can start actual scan _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); - if (!library.EnableMetadata) - { - _logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); - } - // This doesn't work for something like M:/Manga/ and a series has library folder as root var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); if (!shouldUseLibraryScan) { - _logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name); + _logger.LogError("Library {LibraryName} consists of one or more Series folders, using series scan", library.Name); } - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1: Scan & Parse Files", library.Name); - var (scanElapsedTime, parsedSeries) = await ScanFiles(library, libraryFolderPaths, - shouldUseLibraryScan, forceUpdate); - - // We need to remove any keys where there is no actual parser info - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 2: Process and Update Database", library.Name); - var totalFiles = await ProcessParsedSeries(forceUpdate, parsedSeries, library, scanElapsedTime); - - UpdateLastScanned(library); - _unitOfWork.LibraryRepository.Update(library); - - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 3: Save Library", library.Name); - if (await _unitOfWork.CommitAsync()) - { - if (totalFiles == 0) - { - _logger.LogInformation( - "[ScannerService] Finished library scan of {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}. There were no changes", - parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); - } - else - { - _logger.LogInformation( - "[ScannerService] Finished library scan of {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", - totalFiles, parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); - } - - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 5: Remove Deleted Series", library.Name); - await RemoveSeriesNotFound(parsedSeries, library); - } - else - { - _logger.LogCritical( - "[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan"); - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, string.Empty)); - await _metadataService.RemoveAbandonedMetadataKeys(); - - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); - } - - private async Task RemoveSeriesNotFound(Dictionary> parsedSeries, Library library) - { - try - { - _logger.LogDebug("[ScannerService] Removing series that were not found during the scan"); - - var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(parsedSeries.Keys.ToList(), library.Id); - _logger.LogDebug("[ScannerService] Found {Count} series to remove: {SeriesList}", - removedSeries.Count, string.Join(", ", removedSeries.Select(s => s.Name))); - - // Commit the changes - await _unitOfWork.CommitAsync(); - - // Notify for each removed series - foreach (var series in removedSeries) - { - await _eventHub.SendMessageAsync( - MessageFactory.SeriesRemoved, - MessageFactory.SeriesRemovedEvent(series.Id, series.Name, series.LibraryId), - false - ); - } - - _logger.LogDebug("[ScannerService] Series removal process completed"); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "[ScannerService] Error during series cleanup. Please check logs and rescan"); - } - } - - private async Task ProcessParsedSeries(bool forceUpdate, Dictionary> parsedSeries, Library library, long scanElapsedTime) - { - // Iterate over the dictionary and remove only the ParserInfos that don't need processing - var toProcess = new Dictionary>(); - var scanSw = Stopwatch.StartNew(); - - foreach (var series in parsedSeries) - { - if (!series.Key.HasChanged) - { - _logger.LogDebug("{Series} hasn't changed", series.Key.Name); - continue; - } - - // Filter out ParserInfos where FullFilePath is empty (i.e., folder not modified) - var validInfos = series.Value.Where(info => !string.IsNullOrEmpty(info.Filename)).ToList(); - - if (validInfos.Count != 0) - { - toProcess[series.Key] = validInfos; - } - } - - if (toProcess.Count > 0) - { - // For all Genres in the ParserInfos, do a bulk check against the DB on what is not in the DB and create them - // This will ensure all Genres are pre-created and allow our Genre lookup (and Priming) to be much simpler. It will be slower, but more consistent. - var allGenres = toProcess - .SelectMany(s => s.Value - .SelectMany(p => p.ComicInfo?.Genre? - .Split(",", StringSplitOptions.RemoveEmptyEntries) // Split on comma and remove empty entries - .Select(g => g.Trim()) // Trim each genre - .Where(g => !string.IsNullOrWhiteSpace(g)) // Ensure no null/empty genres - ?? [])); // Handle null Genre or ComicInfo safely - - await CreateAllGenresAsync(allGenres.Distinct().ToList()); - - var allTags = toProcess - .SelectMany(s => s.Value - .SelectMany(p => p.ComicInfo?.Tags? - .Split(",", StringSplitOptions.RemoveEmptyEntries) // Split on comma and remove empty entries - .Select(g => g.Trim()) // Trim each genre - .Where(g => !string.IsNullOrWhiteSpace(g)) // Ensure no null/empty genres - ?? [])); // Handle null Tag or ComicInfo safely - - await CreateAllTagsAsync(allTags.Distinct().ToList()); - } - var totalFiles = 0; - var seriesLeftToProcess = toProcess.Count; - _logger.LogInformation("[ScannerService] Found {SeriesCount} Series that need processing in {Time} ms", toProcess.Count, scanSw.ElapsedMilliseconds + scanElapsedTime); + var seenSeries = new List(); - foreach (var pSeries in toProcess) + + await _processSeries.Prime(); + var processTasks = new List>(); + + var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate); + + // NOTE: This runs sync after every file is scanned + foreach (var task in processTasks) { - totalFiles += pSeries.Value.Count; - var seriesProcessStopWatch = Stopwatch.StartNew(); - await _processSeries.ProcessSeriesAsync(pSeries.Value, library, seriesLeftToProcess, forceUpdate); - _logger.LogTrace("[TIME] Kavita took {Time} ms to process {SeriesName}", seriesProcessStopWatch.ElapsedMilliseconds, pSeries.Value[0].Series); - seriesLeftToProcess--; + await task(); } - + // TODO: We might be able to do Task.WhenAll await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent(string.Empty, library.Name, ProgressEventType.Ended)); _logger.LogInformation("[ScannerService] Finished file scan in {ScanAndUpdateTime} milliseconds. Updating database", scanElapsedTime); - return totalFiles; - } - - - private static void UpdateLastScanned(Library library) - { var time = DateTime.Now; foreach (var folderPath in library.Folders) { @@ -688,87 +511,109 @@ public class ScannerService : IScannerService } library.UpdateLastScanned(time); + + + _unitOfWork.LibraryRepository.Update(library); + if (await _unitOfWork.CommitAsync()) + { + if (totalFiles == 0) + { + _logger.LogInformation( + "[ScannerService] Finished library scan of {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}. There were no changes", + seenSeries.Count, sw.ElapsedMilliseconds, library.Name); + } + else + { + _logger.LogInformation( + "[ScannerService] Finished library scan of {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", + totalFiles, seenSeries.Count, sw.ElapsedMilliseconds, library.Name); + } + + try + { + // Could I delete anything in a Library's Series where the LastScan date is before scanStart? + // NOTE: This implementation is expensive + _logger.LogDebug("[ScannerService] Removing Series that were not found during the scan"); + var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(seenSeries, library.Id); + _logger.LogDebug("[ScannerService] Found {Count} series that needs to be removed: {SeriesList}", + removedSeries.Count, removedSeries.Select(s => s.Name)); + _logger.LogDebug("[ScannerService] Removing Series that were not found during the scan - complete"); + + await _unitOfWork.CommitAsync(); + + foreach (var s in removedSeries) + { + await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false); + } + } + catch (Exception ex) + { + _logger.LogCritical(ex, "[ScannerService] There was an issue deleting series for cleanup. Please check logs and rescan"); + } + } + else + { + _logger.LogCritical( + "[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan"); + } + + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, string.Empty)); + await _metadataService.RemoveAbandonedMetadataKeys(); + + BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); + return; + + Task TrackFiles(Tuple> parsedInfo) + { + var skippedScan = parsedInfo.Item1; + var parsedFiles = parsedInfo.Item2; + if (parsedFiles.Count == 0) return Task.CompletedTask; + + var foundParsedSeries = new ParsedSeries() + { + Name = parsedFiles[0].Series, + NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles[0].Series), + Format = parsedFiles[0].Format + }; + + if (skippedScan) + { + seenSeries.AddRange(parsedFiles.Select(pf => new ParsedSeries() + { + Name = pf.Series, + NormalizedName = Scanner.Parser.Parser.Normalize(pf.Series), + Format = pf.Format + })); + return Task.CompletedTask; + } + + totalFiles += parsedFiles.Count; + + + seenSeries.Add(foundParsedSeries); + processTasks.Add(async () => await _processSeries.ProcessSeriesAsync(parsedFiles, library, forceUpdate)); + return Task.CompletedTask; + } } - private async Task>>> ScanFiles(Library library, IList dirs, - bool isLibraryScan, bool forceChecks = false) + private async Task ScanFiles(Library library, IEnumerable dirs, + bool isLibraryScan, Func>, Task>? processSeriesInfos = null, bool forceChecks = false) { var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); var scanWatch = Stopwatch.StartNew(); - var processedSeries = await scanner.ScanLibrariesForSeries(library, dirs, - isLibraryScan, await _unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), forceChecks); + await scanner.ScanLibrariesForSeries(library.Type, dirs, library.Name, + isLibraryScan, await _unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), processSeriesInfos, forceChecks); var scanElapsedTime = scanWatch.ElapsedMilliseconds; - var parsedSeries = TrackFoundSeriesAndFiles(processedSeries); - - return Tuple.Create(scanElapsedTime, parsedSeries); + return scanElapsedTime; } - /// - /// Given a list of all Genres, generates new Genre entries for any that do not exist. - /// Does not delete anything, that will be handled by nightly task - /// - /// - private async Task CreateAllGenresAsync(ICollection genres) + public static IEnumerable FindSeriesNotOnDisk(IEnumerable existingSeries, Dictionary> parsedSeries) { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Genres"); - - try - { - // Pass the non-normalized genres directly to the repository - var nonExistingGenres = await _unitOfWork.GenreRepository.GetAllGenresNotInListAsync(genres); - - // Create and attach new genres using the non-normalized names - foreach (var genre in nonExistingGenres) - { - var newGenre = new GenreBuilder(genre).Build(); - _unitOfWork.GenreRepository.Attach(newGenre); - } - - // Commit changes - if (nonExistingGenres.Count > 0) - { - await _unitOfWork.CommitAsync(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Genres"); - } + return existingSeries.Where(es => !ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(es, parsedSeries)); } - /// - /// Given a list of all Tags, generates new Tag entries for any that do not exist. - /// Does not delete anything, that will be handled by nightly task - /// - /// - private async Task CreateAllTagsAsync(ICollection tags) - { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Tags"); - - try - { - // Pass the non-normalized tags directly to the repository - var nonExistingTags = await _unitOfWork.TagRepository.GetAllTagsNotInListAsync(tags); - - // Create and attach new genres using the non-normalized names - foreach (var tag in nonExistingTags) - { - var newTag = new TagBuilder(tag).Build(); - _unitOfWork.TagRepository.Attach(newTag); - } - - // Commit changes - if (nonExistingTags.Count > 0) - { - await _unitOfWork.CommitAsync(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Tags"); - } - } } diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index 3dca14ab9..40017f0ef 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -1,108 +1,34 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text.Json.Serialization; using System.Threading.Tasks; using API.Data; -using API.DTOs.Theme; using API.Entities; using API.Entities.Enums.Theme; using API.Extensions; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; -using Flurl.Http; -using HtmlAgilityPack; using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using MarkdownDeep; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Authorization; namespace API.Services.Tasks; -#nullable enable - -internal class GitHubContent -{ - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("path")] - public string Path { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonPropertyName("download_url")] - [JsonProperty("download_url")] - public string DownloadUrl { get; set; } - - [JsonProperty("sha")] - public string Sha { get; set; } -} - -/// -/// The readme of the Theme repo -/// -internal class ThemeMetadata -{ - public string Author { get; set; } - public string AuthorUrl { get; set; } - public string Description { get; set; } - public Version LastCompatible { get; set; } -} - public interface IThemeService { Task GetContent(int themeId); + Task Scan(); Task UpdateDefault(int themeId); - /// - /// Browse theme repo for themes to download - /// - /// - Task> GetDownloadableThemes(); - - Task DownloadRepoTheme(DownloadableSiteThemeDto dto); - Task DeleteTheme(int siteThemeId); - Task CreateThemeFromFile(string tempFile, string username); - Task SyncThemes(); } - - public class ThemeService : IThemeService { private readonly IDirectoryService _directoryService; private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _eventHub; - private readonly IFileService _fileService; - private readonly ILogger _logger; - private readonly Markdown _markdown = new(); - private readonly IMemoryCache _cache; - private readonly MemoryCacheEntryOptions _cacheOptions; - private const string GithubBaseUrl = "https://api.github.com"; - - /// - /// Used for refreshing metadata around themes - /// - private const string GithubReadme = "https://raw.githubusercontent.com/Kareadita/Themes/main/README.md"; - - public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, - IEventHub eventHub, IFileService fileService, ILogger logger, IMemoryCache cache) + public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IEventHub eventHub) { _directoryService = directoryService; _unitOfWork = unitOfWork; _eventHub = eventHub; - _fileService = fileService; - _logger = logger; - _cache = cache; - - _cacheOptions = new MemoryCacheEntryOptions() - .SetSize(1) - .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); } /// @@ -112,7 +38,8 @@ public class ThemeService : IThemeService /// public async Task GetContent(int themeId) { - var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist"); + var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId); + if (theme == null) throw new KavitaException("theme-doesnt-exist"); var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile)) throw new KavitaException("theme-doesnt-exist"); @@ -120,366 +47,78 @@ public class ThemeService : IThemeService return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile); } - public async Task> GetDownloadableThemes() - { - const string cacheKey = "browse"; - // Avoid a duplicate Dark issue some users faced during migration - var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()) - .GroupBy(k => k.Name) - .ToDictionary(g => g.Key, g => g.First()); - - if (_cache.TryGetValue(cacheKey, out List? themes) && themes != null) - { - foreach (var t in themes) - { - t.AlreadyDownloaded = existingThemes.ContainsKey(t.Name); - } - return themes; - } - - // Fetch contents of the Native Themes directory - var themesContents = await GetDirectoryContent("Native%20Themes"); - - // Filter out directories - var themeDirectories = themesContents.Where(c => c.Type == "dir").ToList(); - - // Get the Readme and augment the theme data - var themeMetadata = await GetReadme(); - - var themeDtos = new List(); - foreach (var themeDir in themeDirectories) - { - var themeName = themeDir.Name.Trim(); - - // Fetch contents of the theme directory - var themeContents = await GetDirectoryContent(themeDir.Path); - - - // Find css and preview files - var cssFile = themeContents.FirstOrDefault(c => c.Name.EndsWith(".css")); - var previewUrls = GetPreviewUrls(themeContents); - - if (cssFile == null) continue; - - var cssUrl = cssFile.DownloadUrl; - - - var dto = new DownloadableSiteThemeDto() - { - Name = themeName, - CssUrl = cssUrl, - CssFile = cssFile.Name, - PreviewUrls = previewUrls, - Sha = cssFile.Sha, - Path = themeDir.Path, - }; - - if (themeMetadata.TryGetValue(themeName, out var metadata)) - { - dto.Author = metadata.Author; - dto.LastCompatibleVersion = metadata.LastCompatible.ToString(); - dto.IsCompatible = BuildInfo.Version <= metadata.LastCompatible; - dto.AlreadyDownloaded = existingThemes.ContainsKey(themeName); - dto.Description = metadata.Description; - } - - themeDtos.Add(dto); - } - - _cache.Set(cacheKey, themeDtos, _cacheOptions); - - return themeDtos; - } - - private static List GetPreviewUrls(IEnumerable themeContents) - { - return themeContents - .Where(c => Parser.IsImage(c.Name) ) - .Select(p => p.DownloadUrl) - .ToList(); - } - - private static async Task> GetDirectoryContent(string path) - { - var json = await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}" - .WithHeader("Accept", "application/vnd.github+json") - .WithHeader("User-Agent", "Kavita") - .GetStringAsync(); - - return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json); - } - /// - /// Returns a map of all Native Themes names mapped to their metadata + /// Scans the site theme directory for custom css files and updates what the system has on store /// - /// - private async Task> GetReadme() + public async Task Scan() { - // Try and delete a Readme file if it already exists - var existingReadmeFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, "README.md"); - if (_directoryService.FileSystem.File.Exists(existingReadmeFile)) - { - _directoryService.DeleteFiles([existingReadmeFile]); - } - - var tempDownloadFile = await GithubReadme.DownloadFileAsync(_directoryService.TempDirectory); - - // Read file into Markdown - var htmlContent = _markdown.Transform(await _directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile)); - var htmlDoc = new HtmlDocument(); - htmlDoc.LoadHtml(htmlContent); - - // Find the table of Native Themes - var tableContent = htmlDoc.DocumentNode - .SelectSingleNode("//h2[contains(text(),'Native Themes')]/following-sibling::p").InnerText; - - // Initialize dictionary to store theme metadata - var themes = new Dictionary(); - - - // Split the table content by rows - var rows = tableContent.Split("\r\n").Select(row => row.Trim()).Where(row => !string.IsNullOrWhiteSpace(row)).ToList(); - - // Parse each row in the Native Themes table - foreach (var row in rows.Skip(2)) - { - - var cells = row.Split('|').Skip(1).Select(cell => cell.Trim()).ToList(); - - // Extract information from each cell - var themeName = cells[0]; - var authorName = cells[1]; - var description = cells[2]; - var compatibility = Version.Parse(cells[3]); - - // Create ThemeMetadata object - var themeMetadata = new ThemeMetadata - { - Author = authorName, - Description = description, - LastCompatible = compatibility - }; - - // Add theme metadata to dictionary - themes.Add(themeName, themeMetadata); - } - - return themes; - } - - - private async Task DownloadSiteTheme(DownloadableSiteThemeDto dto) - { - if (string.IsNullOrEmpty(dto.Sha)) - { - throw new ArgumentException("SHA cannot be null or empty for already downloaded themes."); - } - _directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory); - var existingTempFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, - _directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name); - _directoryService.DeleteFiles([existingTempFile]); + var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList(); + var themeFiles = _directoryService + .GetFilesWithExtension(Scanner.Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css") + .Where(name => !reservedNames.Contains(name.ToNormalized()) && !name.Contains(" ")) + .ToList(); - var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(_directoryService.TempDirectory); + var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList(); - // Validate the hash on the downloaded file - // if (!_fileService.ValidateSha(tempDownloadFile, dto.Sha)) - // { - // throw new KavitaException("Cannot download theme, hash does not match"); - // } - - _directoryService.CopyFileToDirectory(tempDownloadFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, dto.CssFile); - - return finalLocation; - } - - - public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto) - { - - // Validate we don't have a collision with existing or existing doesn't already exist - var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty); - if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile)) + // First remove any files from allThemes that are User Defined and not on disk + var userThemes = allThemes.Where(t => t.Provider == ThemeProvider.User).ToList(); + foreach (var userTheme in userThemes) { - // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly - _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); + var filepath = Scanner.Parser.Parser.NormalizePath( + _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, userTheme.FileName)); + if (_directoryService.FileSystem.File.Exists(filepath)) continue; + + // I need to do the removal different. I need to update all user preferences to use DefaultTheme + allThemes.Remove(userTheme); + await RemoveTheme(userTheme); } - var finalLocation = await DownloadSiteTheme(dto); - - // Create a new entry and note that this is downloaded - var theme = new SiteTheme() + // Add new custom themes + var allThemeNames = allThemes.Select(t => t.NormalizedName).ToList(); + foreach (var themeFile in themeFiles) { - Name = dto.Name, - NormalizedName = dto.Name.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), - Provider = ThemeProvider.Custom, - IsDefault = false, - GitHubPath = dto.Path, - Description = dto.Description, - PreviewUrls = string.Join('|', dto.PreviewUrls), - Author = dto.Author, - ShaHash = dto.Sha, - CompatibleVersion = dto.LastCompatibleVersion, - }; - _unitOfWork.SiteThemeRepository.Add(theme); + var themeName = + _directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile).ToNormalized(); + if (allThemeNames.Contains(themeName)) continue; - await _unitOfWork.CommitAsync(); - - // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); - return theme; - } - - public async Task SyncThemes() - { - var themes = await _unitOfWork.SiteThemeRepository.GetThemes(); - var themeMetadata = await GetReadme(); - foreach (var theme in themes) - { - await SyncTheme(theme, themeMetadata); - } - _logger.LogInformation("Sync Themes complete"); - } - - /// - /// If the Theme is from the Theme repo, see if there is a new version that is compatible - /// - /// - /// The Readme information - private async Task SyncTheme(SiteTheme? theme, IDictionary themeMetadata) - { - // Given a theme, first validate that it is applicable - if (theme == null || theme.Provider == ThemeProvider.System || string.IsNullOrEmpty(theme.GitHubPath)) - { - _logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name); - return; - } - - if (new Version(theme.CompatibleVersion) > BuildInfo.Version) - { - _logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion); - return; - } - - - var themeContents = await GetDirectoryContent(theme.GitHubPath); - var cssFile = themeContents.FirstOrDefault(c => c.Name.EndsWith(".css")); - - if (cssFile == null) return; - - // Update any metadata - if (themeMetadata.TryGetValue(theme.Name, out var metadata)) - { - theme.Description = metadata.Description; - theme.Author = metadata.Author; - theme.CompatibleVersion = metadata.LastCompatible.ToString(); - theme.PreviewUrls = string.Join('|', GetPreviewUrls(themeContents)); - } - - var hasUpdated = cssFile.Sha != theme.ShaHash; - if (hasUpdated) - { - _logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name); - var tempLocation = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); - - _directoryService.DeleteFiles([tempLocation]); - - var location = await cssFile.DownloadUrl.DownloadFileAsync(_directoryService.TempDirectory); - if (_directoryService.FileSystem.File.Exists(location)) + _unitOfWork.SiteThemeRepository.Add(new SiteTheme() { - _directoryService.CopyFileToDirectory(location, _directoryService.SiteThemeDirectory); - _logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name); + Name = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile), + NormalizedName = themeName, + FileName = _directoryService.FileSystem.Path.GetFileName(themeFile), + Provider = ThemeProvider.User, + IsDefault = false, + }); + + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(themeFile), themeName, + ProgressEventType.Updated)); + } + + + if (_unitOfWork.HasChanges()) + { + await _unitOfWork.CommitAsync(); + } + + // if there are no default themes, reselect Dark as default + var postSaveThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList(); + if (!postSaveThemes.Exists(t => t.IsDefault)) + { + var defaultThemeName = Seed.DefaultThemes.Single(t => t.IsDefault).NormalizedName; + var theme = postSaveThemes.SingleOrDefault(t => t.NormalizedName == defaultThemeName); + if (theme != null) + { + theme.IsDefault = true; + _unitOfWork.SiteThemeRepository.Update(theme); + await _unitOfWork.CommitAsync(); } + } - await _unitOfWork.CommitAsync(); - - - if (hasUpdated) - { - await _eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated, - MessageFactory.SiteThemeUpdatedEvent(theme.Name)); - } - - // Send an update to refresh metadata around the themes await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); - - _logger.LogInformation("Theme Sync complete"); - } - - /// - /// Deletes a SiteTheme. The CSS file will be moved to temp/ to allow user to recover data - /// - /// - public async Task DeleteTheme(int siteThemeId) - { - // Validate no one else is using this theme - var inUse = await _unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId); - if (inUse) - { - throw new KavitaException("errors.delete-theme-in-use"); - } - - var siteTheme = await _unitOfWork.SiteThemeRepository.GetTheme(siteThemeId); - if (siteTheme == null) return; - - await RemoveTheme(siteTheme); - } - - /// - /// This assumes a file is already in temp directory and will be used for - /// - /// - /// - public async Task CreateThemeFromFile(string tempFile, string username) - { - if (!_directoryService.FileSystem.File.Exists(tempFile)) - { - _logger.LogInformation("Unable to create theme from manual upload as file not in temp"); - throw new KavitaException("errors.theme-manual-upload"); - } - - - var filename = _directoryService.FileSystem.FileInfo.New(tempFile).Name; - var themeName = Path.GetFileNameWithoutExtension(filename); - - if (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null) - { - throw new KavitaException("errors.theme-already-in-use"); - } - - _directoryService.CopyFileToDirectory(tempFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, filename); - - - // Create a new entry and note that this is downloaded - var theme = new SiteTheme() - { - Name = Path.GetFileNameWithoutExtension(filename), - NormalizedName = themeName.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), - Provider = ThemeProvider.Custom, - IsDefault = false, - Description = $"Manually uploaded via UI by {username}", - PreviewUrls = string.Empty, - Author = username, - }; - _unitOfWork.SiteThemeRepository.Add(theme); - - await _unitOfWork.CommitAsync(); - - // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); - return theme; - + MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended)); } @@ -490,7 +129,6 @@ public class ThemeService : IThemeService /// private async Task RemoveTheme(SiteTheme theme) { - _logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name); var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id); var defaultTheme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); foreach (var pref in prefs) @@ -498,20 +136,6 @@ public class ThemeService : IThemeService pref.Theme = defaultTheme; _unitOfWork.UserRepository.Update(pref); } - - try - { - // Copy the theme file to temp for nightly removal (to give user time to reclaim if made a mistake) - var existingLocation = - _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); - var newLocation = - _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); - _directoryService.CopyFileToDirectory(existingLocation, newLocation); - _directoryService.DeleteFiles([existingLocation]); - } - catch (Exception) { /* Swallow */ } - - _unitOfWork.SiteThemeRepository.Remove(theme); await _unitOfWork.CommitAsync(); } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 5d5df6647..bc14967a3 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -1,38 +1,28 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; +using System.IO; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; using System.Threading.Tasks; using API.Data; -using API.Data.Misc; using API.Data.Repositories; using API.DTOs.Stats; -using API.DTOs.Stats.V3; -using API.Entities; using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; +using API.Entities.Enums.UserPreferences; using Flurl.Http; -using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -#nullable enable - public interface IStatsService { Task Send(); + Task GetServerInfo(); Task GetServerInfoSlim(); Task SendCancellation(); } @@ -44,33 +34,23 @@ public class StatsService : IStatsService private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly DataContext _context; - private readonly ILicenseService _licenseService; - private readonly UserManager _userManager; - private readonly IEmailService _emailService; - private readonly ICacheService _cacheService; - private readonly string _apiUrl = ""; - private const string ApiKey = "MsnvA2DfQqxSK5jh"; // It's not important this is public, just a way to keep bots from hitting the API willy-nilly + private readonly IStatisticService _statisticService; + private const string ApiUrl = "https://stats.kavitareader.com"; // "" - public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataContext context, - ILicenseService licenseService, UserManager userManager, IEmailService emailService, - ICacheService cacheService, IHostEnvironment environment) + public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService) { _logger = logger; _unitOfWork = unitOfWork; _context = context; - _licenseService = licenseService; - _userManager = userManager; - _emailService = emailService; - _cacheService = cacheService; + _statisticService = statisticService; - FlurlConfiguration.ConfigureClientForUrl(Configuration.StatsApiUrl); - - _apiUrl = environment.IsDevelopment() ? "http://localhost:5001" : Configuration.StatsApiUrl; + FlurlHttp.ConfigureClient(ApiUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } /// /// Due to all instances firing this at the same time, we can DDOS our server. This task when fired will schedule the task to be run - /// randomly over a six-hour spread + /// randomly over a 6 hour spread /// public async Task Send() { @@ -89,22 +69,24 @@ public class StatsService : IStatsService // ReSharper disable once MemberCanBePrivate.Global public async Task SendData() { - var sw = Stopwatch.StartNew(); - var data = await GetStatV3Payload(); - _logger.LogDebug("Collecting stats took {Time} ms", sw.ElapsedMilliseconds); - sw.Stop(); + var data = await GetServerInfo(); await SendDataToStatsServer(data); } - private async Task SendDataToStatsServer(ServerInfoV3Dto data) + private async Task SendDataToStatsServer(ServerInfoDto data) { var responseContent = string.Empty; try { - var response = await (_apiUrl + "/api/v3/stats") - .WithBasicHeaders(ApiKey) + var response = await (ApiUrl + "/api/v2/stats") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(30)) .PostJsonAsync(data); if (response.StatusCode != StatusCodes.Status200OK) @@ -128,6 +110,67 @@ public class StatsService : IStatsService } } + public async Task GetServerInfo() + { + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + + var serverInfo = new ServerInfoDto + { + InstallId = serverSettings.InstallId, + Os = RuntimeInformation.OSDescription, + KavitaVersion = serverSettings.InstallVersion, + DotnetVersion = Environment.Version.ToString(), + IsDocker = OsInfo.IsDocker, + NumOfCores = Math.Max(Environment.ProcessorCount, 1), + UsersWithEmulateComicBook = await _context.AppUserPreferences.CountAsync(p => p.EmulateBook), + TotalReadingHours = await _statisticService.TimeSpentReadingForUsersAsync(ArraySegment.Empty, ArraySegment.Empty), + + PercentOfLibrariesWithFolderWatchingEnabled = await GetPercentageOfLibrariesWithFolderWatchingEnabled(), + PercentOfLibrariesIncludedInRecommended = await GetPercentageOfLibrariesIncludedInRecommended(), + PercentOfLibrariesIncludedInDashboard = await GetPercentageOfLibrariesIncludedInDashboard(), + PercentOfLibrariesIncludedInSearch = await GetPercentageOfLibrariesIncludedInSearch(), + + HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(), + NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(), + NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(), + NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(), + OPDSEnabled = serverSettings.EnableOpds, + NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Count(), + TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(), + TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(), + TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(), + UsingSeriesRelationships = await GetIfUsingSeriesRelationship(), + EncodeMediaAs = serverSettings.EncodeMediaAs, + MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(), + MaxVolumesInASeries = await MaxVolumesInASeries(), + MaxChaptersInASeries = await MaxChaptersInASeries(), + MangaReaderBackgroundColors = await AllMangaReaderBackgroundColors(), + MangaReaderPageSplittingModes = await AllMangaReaderPageSplitting(), + MangaReaderLayoutModes = await AllMangaReaderLayoutModes(), + FileFormats = AllFormats(), + UsingRestrictedProfiles = await GetUsingRestrictedProfiles(), + LastReadTime = await _unitOfWork.AppUserProgressRepository.GetLatestProgress() + }; + + var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList(); + serverInfo.UsersOnCardLayout = + usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.Cards); + serverInfo.UsersOnListLayout = + usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.List); + + var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault(); + + if (firstAdminUser != null) + { + var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName!)); + var activeTheme = firstAdminUserPref?.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault); + + serverInfo.ActiveSiteTheme = activeTheme.Name; + if (firstAdminUserPref != null) serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode; + } + + return serverInfo; + } public async Task GetServerInfoSlim() { @@ -136,9 +179,7 @@ public class StatsService : IStatsService { InstallId = serverSettings.InstallId, KavitaVersion = serverSettings.InstallVersion, - IsDocker = OsInfo.IsDocker, - FirstInstallDate = serverSettings.FirstInstallDate, - FirstInstallVersion = serverSettings.FirstInstallVersion + IsDocker = OsInfo.IsDocker }; } @@ -151,8 +192,12 @@ public class StatsService : IStatsService try { - var response = await (_apiUrl + "/api/v2/stats/opt-out?installId=" + installId) - .WithBasicHeaders(ApiKey) + var response = await (ApiUrl + "/api/v2/stats/opt-out?installId=" + installId) + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") .WithTimeout(TimeSpan.FromSeconds(30)) .PostAsync(); @@ -171,32 +216,42 @@ public class StatsService : IStatsService } } - private static async Task PingStatsApi() + private async Task GetPercentageOfLibrariesWithFolderWatchingEnabled() { - try - { - var sw = Stopwatch.StartNew(); - var response = await (Configuration.StatsApiUrl + "/api/health/") - .WithBasicHeaders(ApiKey) - .GetAsync(); + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + if (libraries.Count == 0) return 0.0f; + return libraries.Count(l => l.FolderWatching) / (1.0f * libraries.Count); + } - if (response.StatusCode == StatusCodes.Status200OK) - { - sw.Stop(); - return sw.ElapsedMilliseconds; - } - } - catch (Exception) - { - /* Swallow */ - } + private async Task GetPercentageOfLibrariesIncludedInRecommended() + { + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + if (libraries.Count == 0) return 0.0f; + return libraries.Count(l => l.IncludeInRecommended) / (1.0f * libraries.Count); + } - return 0; + private async Task GetPercentageOfLibrariesIncludedInDashboard() + { + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + if (libraries.Count == 0) return 0.0f; + return libraries.Count(l => l.IncludeInDashboard) / (1.0f * libraries.Count); + } + + private async Task GetPercentageOfLibrariesIncludedInSearch() + { + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + if (libraries.Count == 0) return 0.0f; + return libraries.Count(l => l.IncludeInSearch) / (1.0f * libraries.Count); + } + + private Task GetIfUsingSeriesRelationship() + { + return _context.SeriesRelation.AnyAsync(); } private async Task MaxSeriesInAnyLibrary() { - // If first time flow, return 0 + // If first time flow, just return 0 if (!await _context.Series.AnyAsync()) return 0; return await _context.Series .Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count()) @@ -222,191 +277,50 @@ public class StatsService : IStatsService { // If first time flow, just return 0 if (!await _context.Chapter.AnyAsync()) return 0; - return await _context.Series .AsNoTracking() .AsSplitQuery() .MaxAsync(s => s.Volumes! - .Where(v => v.MinNumber == Parser.LooseLeafVolumeNumber) + .Where(v => v.Number == 0) .SelectMany(v => v.Chapters!) .Count()); } - private async Task GetStatV3Payload() + private async Task> AllMangaReaderBackgroundColors() { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var mediaSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); - var dto = new ServerInfoV3Dto() - { - InstallId = serverSettings.InstallId, - KavitaVersion = serverSettings.InstallVersion, - InitialKavitaVersion = serverSettings.FirstInstallVersion, - InitialInstallDate = (DateTime)serverSettings.FirstInstallDate!, - IsDocker = OsInfo.IsDocker, - Os = RuntimeInformation.OSDescription, - NumOfCores = Math.Max(Environment.ProcessorCount, 1), - DotnetVersion = Environment.Version.ToString(), - OpdsEnabled = serverSettings.EnableOpds, - EncodeMediaAs = serverSettings.EncodeMediaAs, - MatchedMetadataEnabled = mediaSettings.Enabled - }; - - dto.OsLocale = CultureInfo.CurrentCulture.EnglishName; - dto.LastReadTime = await _unitOfWork.AppUserProgressRepository.GetLatestProgress(); - dto.MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(); - dto.MaxVolumesInASeries = await MaxVolumesInASeries(); - dto.MaxChaptersInASeries = await MaxChaptersInASeries(); - dto.TotalFiles = await _context.MangaFile.CountAsync(); - dto.TotalGenres = await _context.Genre.CountAsync(); - dto.TotalPeople = await _context.Person.CountAsync(); - dto.TotalSeries = await _context.Series.CountAsync(); - dto.TotalLibraries = await _context.Library.CountAsync(); - dto.NumberOfCollections = await _context.AppUserCollection.CountAsync(); - dto.NumberOfReadingLists = await _context.ReadingList.CountAsync(); - - try - { - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - dto.ActiveKavitaPlusSubscription = await _licenseService.HasActiveSubscription(license); - } - catch (Exception) - { - dto.ActiveKavitaPlusSubscription = false; - } - - - // Find a random cbz/zip file and open it for reading - await OpenRandomFile(dto); - dto.TimeToPingKavitaStatsApi = await PingStatsApi(); - - #region Relationships - - dto.Relationships = await _context.SeriesRelation - .GroupBy(sr => sr.RelationKind) - .Select(g => new RelationshipStatV3 - { - Relationship = g.Key, - Count = g.Count() - }) - .ToListAsync(); - - #endregion - - #region Libraries - var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.Folders | - LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns | LibraryIncludes.AppUser)).ToList(); - dto.Libraries ??= []; - foreach (var library in allLibraries) - { - var libDto = new LibraryStatV3(); - libDto.IncludeInDashboard = library.IncludeInDashboard; - libDto.IncludeInSearch = library.IncludeInSearch; - libDto.LastScanned = library.LastScanned; - libDto.NumberOfFolders = library.Folders.Count; - libDto.FileTypes = library.LibraryFileTypes.Select(s => s.FileTypeGroup).Distinct().ToList(); - libDto.UsingExcludePatterns = library.LibraryExcludePatterns.Any(p => !string.IsNullOrEmpty(p.Pattern)); - libDto.UsingFolderWatching = library.FolderWatching; - libDto.CreateCollectionsFromMetadata = library.ManageCollections; - libDto.CreateReadingListsFromMetadata = library.ManageReadingLists; - libDto.LibraryType = library.Type; - - dto.Libraries.Add(libDto); - } - #endregion - - #region Users - - // Create a dictionary mapping user IDs to the libraries they have access to - var userLibraryAccess = allLibraries - .SelectMany(l => l.AppUsers.Select(appUser => new { l, appUser.Id })) - .GroupBy(x => x.Id) - .ToDictionary(g => g.Key, g => g.Select(x => x.l).ToList()); - dto.Users ??= []; - var allUsers = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences - | AppUserIncludes.ReadingLists | AppUserIncludes.Bookmarks - | AppUserIncludes.Collections | AppUserIncludes.Devices - | AppUserIncludes.Progress | AppUserIncludes.Ratings - | AppUserIncludes.SmartFilters | AppUserIncludes.WantToRead, false); - foreach (var user in allUsers) - { - var userDto = new UserStatV3(); - userDto.HasMALToken = !string.IsNullOrEmpty(user.MalAccessToken); - userDto.HasAniListToken = !string.IsNullOrEmpty(user.AniListAccessToken); - userDto.AgeRestriction = new AgeRestriction() - { - AgeRating = user.AgeRestriction, - IncludeUnknowns = user.AgeRestrictionIncludeUnknowns - }; - - userDto.Locale = user.UserPreferences.Locale; - userDto.Roles = [.. _userManager.GetRolesAsync(user).Result]; - userDto.LastLogin = user.LastActiveUtc; - userDto.HasValidEmail = user.Email != null && _emailService.IsValidEmail(user.Email); - userDto.IsEmailConfirmed = user.EmailConfirmed; - userDto.ActiveTheme = user.UserPreferences.Theme.Name; - userDto.CollectionsCreatedCount = user.Collections.Count; - userDto.ReadingListsCreatedCount = user.ReadingLists.Count; - userDto.LastReadTime = user.Progresses - .Select(p => p.LastModifiedUtc) - .DefaultIfEmpty() - .Max(); - userDto.DevicePlatforms = user.Devices.Select(d => d.Platform).ToList(); - userDto.SeriesBookmarksCreatedCount = user.Bookmarks.Count; - userDto.SmartFilterCreatedCount = user.SmartFilters.Count; - userDto.WantToReadSeriesCount = user.WantToRead.Count; - - if (allLibraries.Count > 0 && userLibraryAccess.TryGetValue(user.Id, out var accessibleLibraries)) - { - userDto.PercentageOfLibrariesHasAccess = (1f * accessibleLibraries.Count) / allLibraries.Count; - } - else - { - userDto.PercentageOfLibrariesHasAccess = 0; - } - - dto.Users.Add(userDto); - } - - #endregion - - return dto; + return await _context.AppUserPreferences.Select(p => p.BackgroundColor).Distinct().ToListAsync(); } - private async Task OpenRandomFile(ServerInfoV3Dto dto) + private async Task> AllMangaReaderPageSplitting() { - var random = new Random(); - List extensions = [".cbz", ".zip"]; + return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync(); + } - // Count the total number of files that match the criteria - var count = await _context.MangaFile.AsNoTracking() - .Where(r => r.Extension != null && extensions.Contains(r.Extension)) - .CountAsync(); - if (count == 0) - { - dto.TimeToOpeCbzMs = 0; - dto.TimeToOpenCbzPages = 0; + private async Task> AllMangaReaderLayoutModes() + { + return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync(); + } - return; - } + private IEnumerable AllFormats() + { - // Generate a random skip value - var skip = random.Next(count); + var results = _context.MangaFile + .AsNoTracking() + .AsEnumerable() + .Select(m => new FileFormatDto() + { + Format = m.Format, + Extension = m.Extension + }) + .DistinctBy(f => f.Extension) + .ToList(); - // Fetch the random file - var randomFile = await _context.MangaFile.AsNoTracking() - .Where(r => r.Extension != null && extensions.Contains(r.Extension)) - .Skip(skip) - .Take(1) - .FirstAsync(); + return results; + } - var sw = Stopwatch.StartNew(); - - await _cacheService.Ensure(randomFile.ChapterId); - var time = sw.ElapsedMilliseconds; - sw.Stop(); - - dto.TimeToOpeCbzMs = time; - dto.TimeToOpenCbzPages = randomFile.Pages; + private Task GetUsingRestrictedProfiles() + { + return _context.Users.AnyAsync(u => u.AgeRestriction > AgeRating.NotApplicable); } } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 4ccf79abb..9bb7b86d5 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -1,22 +1,18 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading.Tasks; using API.DTOs.Update; -using API.Extensions; using API.SignalR; using Flurl.Http; +using HtmlAgilityPack; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using MarkdownDeep; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -#nullable enable internal class GithubReleaseMetadata { @@ -35,7 +31,7 @@ internal class GithubReleaseMetadata /// public required string Body { get; init; } /// - /// Url of the release on GitHub + /// Url of the release on Github /// // ReSharper disable once InconsistentNaming public required string Html_Url { get; init; } @@ -50,13 +46,10 @@ public interface IVersionUpdaterService { Task CheckForUpdate(); Task PushUpdate(UpdateNotificationDto update); - Task> GetAllReleases(int count = 0); - Task GetNumberOfReleasesBehind(bool stableOnly = false); - void BustGithubCache(); + Task> GetAllReleases(); } - -public partial class VersionUpdaterService : IVersionUpdaterService +public class VersionUpdaterService : IVersionUpdaterService { private readonly ILogger _logger; private readonly IEventHub _eventHub; @@ -64,402 +57,33 @@ public partial class VersionUpdaterService : IVersionUpdaterService #pragma warning disable S1075 private const string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest"; private const string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; - private const string GithubPullsUrl = "https://api.github.com/repos/Kareadita/Kavita/pulls/"; - private const string GithubBranchCommitsUrl = "https://api.github.com/repos/Kareadita/Kavita/commits?sha=develop"; #pragma warning restore S1075 - [GeneratedRegex(@"^\n*(.*?)\n+#{1,2}\s", RegexOptions.Singleline)] - private static partial Regex BlogPartRegex(); - private readonly string _cacheFilePath; - /// - /// The latest release cache - /// - private readonly string _cacheLatestReleaseFilePath; - private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(1); - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - public VersionUpdaterService(ILogger logger, IEventHub eventHub, IDirectoryService directoryService) + public VersionUpdaterService(ILogger logger, IEventHub eventHub) { _logger = logger; _eventHub = eventHub; - _cacheFilePath = Path.Combine(directoryService.LongTermCacheDirectory, "github_releases_cache.json"); - _cacheLatestReleaseFilePath = Path.Combine(directoryService.LongTermCacheDirectory, "github_latest_release_cache.json"); - FlurlConfiguration.ConfigureClientForUrl(GithubLatestReleasesUrl); - FlurlConfiguration.ConfigureClientForUrl(GithubAllReleasesUrl); + FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } /// - /// Fetches the latest (stable) release from GitHub. Does not do any extra nightly release parsing. + /// Fetches the latest release from Github /// /// Latest update - public async Task CheckForUpdate() + public async Task CheckForUpdate() { - // Attempt to fetch from cache - var cachedRelease = await TryGetCachedLatestRelease(); - if (cachedRelease != null) - { - return cachedRelease; - } - var update = await GetGithubRelease(); - var dto = CreateDto(update); - - if (dto != null) - { - await CacheLatestReleaseAsync(dto); - } - - return dto; + return CreateDto(update); } - /// - /// Will add any extra (nightly) updates from the latest stable. Does not back-fill anything prior to the latest stable. - /// - /// - private async Task EnrichWithNightlyInfo(List dtos) + public async Task> GetAllReleases() { - var dto = dtos[0]; // Latest version - try - { - var currentVersion = new Version(dto.CurrentVersion); - var nightlyReleases = await GetNightlyReleases(currentVersion, Version.Parse(dto.UpdateVersion)); - - if (nightlyReleases.Count == 0) return; - - // Create new DTOs for each nightly release and insert them at the beginning of the list - var nightlyDtos = new List(); - foreach (var nightly in nightlyReleases) - { - var prInfo = await FetchPullRequestInfo(nightly.PrNumber); - if (prInfo == null) continue; - - var sections = ParseReleaseBody(prInfo.Body); - var blogPart = ExtractBlogPart(prInfo.Body); - - var nightlyDto = new UpdateNotificationDto - { - // TODO: I should pass Title to the FE so that Nightly Release can be localized - UpdateTitle = $"Nightly Release {nightly.Version} - {prInfo.Title}", - UpdateVersion = nightly.Version, - CurrentVersion = dto.CurrentVersion, - UpdateUrl = prInfo.Html_Url, - PublishDate = prInfo.Merged_At, - IsDocker = true, // Nightlies are always Docker Only - IsReleaseEqual = IsVersionEqualToBuildVersion(Version.Parse(nightly.Version)), - IsReleaseNewer = true, // Since we already filtered these in GetNightlyReleases - IsPrerelease = true, // All Nightlies are considered prerelease - Added = sections.TryGetValue("Added", out var added) ? added : [], - Changed = sections.TryGetValue("Changed", out var changed) ? changed : [], - Fixed = sections.TryGetValue("Fixed", out var bugfixes) ? bugfixes : [], - Removed = sections.TryGetValue("Removed", out var removed) ? removed : [], - Theme = sections.TryGetValue("Theme", out var theme) ? theme : [], - Developer = sections.TryGetValue("Developer", out var developer) ? developer : [], - KnownIssues = sections.TryGetValue("KnownIssues", out var knownIssues) ? knownIssues : [], - Api = sections.TryGetValue("Api", out var api) ? api : [], - FeatureRequests = sections.TryGetValue("Feature Requests", out var frs) ? frs : [], - BlogPart = _markdown.Transform(blogPart.Trim()), - UpdateBody = _markdown.Transform(prInfo.Body.Trim()) - }; - - nightlyDtos.Add(nightlyDto); - } - - // Insert nightly releases at the beginning of the list - var sortedNightlyDtos = nightlyDtos.OrderByDescending(x => x.PublishDate).ToList(); - dtos.InsertRange(0, sortedNightlyDtos); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to enrich nightly release information"); - } - } - - - private async Task FetchPullRequestInfo(int prNumber) - { - try - { - return await $"{GithubPullsUrl}{prNumber}" - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .GetJsonAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to fetch PR information for #{PrNumber}", prNumber); - return null; - } - } - - private async Task> GetNightlyReleases(Version currentVersion, Version latestStableVersion) - { - try - { - var nightlyReleases = new List(); - - var commits = await GithubBranchCommitsUrl - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .GetJsonAsync>(); - - var commitList = commits.ToList(); - bool foundLastStable = false; - - for (var i = 0; i < commitList.Count - 1; i++) - { - var commit = commitList[i]; - var message = commit.Commit.Message.Split('\n')[0]; // Take first line only - - // Skip [skip ci] commits - if (message.Contains("[skip ci]")) continue; - - // Check if this is a stable release - if (message.StartsWith('v')) - { - var stableMatch = Regex.Match(message, @"v(\d+\.\d+\.\d+\.\d+)"); - if (stableMatch.Success) - { - var stableVersion = new Version(stableMatch.Groups[1].Value); - // If we find a stable version lower than current, we've gone too far back - if (stableVersion <= currentVersion) - { - foundLastStable = true; - break; - } - } - continue; - } - - // Look for version bumps that follow PRs - if (!foundLastStable && message == "Bump versions by dotnet-bump-version.") - { - // Get the PR commit that triggered this version bump - if (i + 1 < commitList.Count) - { - var prCommit = commitList[i + 1]; - var prMessage = prCommit.Commit.Message.Split('\n')[0]; - - // Extract PR number using improved regex - var prMatch = Regex.Match(prMessage, @"(?:^|\s)\(#(\d+)\)|\s#(\d+)"); - if (!prMatch.Success) continue; - - var prNumber = int.Parse(prMatch.Groups[1].Value != "" ? - prMatch.Groups[1].Value : prMatch.Groups[2].Value); - - // Get the version from AssemblyInfo.cs in this commit - var version = await GetVersionFromCommit(commit.Sha); - if (version == null) continue; - - // Parse version and compare with current version - if (Version.TryParse(version, out var parsedVersion) && - parsedVersion > latestStableVersion) - { - nightlyReleases.Add(new NightlyInfo - { - Version = version, - PrNumber = prNumber, - Date = DateTime.Parse(commit.Commit.Author.Date, CultureInfo.InvariantCulture) - }); - } - } - } - } - - return nightlyReleases.OrderByDescending(x => x.Date).ToList(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get nightly releases"); - return []; - } - } - - public async Task> GetAllReleases(int count = 0) - { - // Attempt to fetch from cache - var cachedReleases = await TryGetCachedReleases(); - // If there is a cached release and the current version is within it, use it, otherwise regenerate - if (cachedReleases != null && cachedReleases.Any(r => IsVersionEqual(r.UpdateVersion, BuildInfo.Version.ToString()))) - { - if (count > 0) - { - // NOTE: We may want to allow the admin to clear Github cache - return cachedReleases.Take(count).ToList(); - } - - return cachedReleases; - } - var updates = await GetGithubReleases(); - var query = updates.Select(CreateDto) - .Where(d => d != null) - .OrderByDescending(d => d!.PublishDate) - .Select(d => d!); - - var updateDtos = query.ToList(); - - // Sometimes a release can be 0.8.5.0 on disk, but 0.8.5 from Github - var versionParts = updateDtos[0].UpdateVersion.Split('.'); - if (versionParts.Length < 4) - { - updateDtos[0].UpdateVersion += ".0"; // Append missing parts - } - - // If we're on a nightly build, enrich the information - if (updateDtos.Count != 0) // && BuildInfo.Version > new Version(updateDtos[0].UpdateVersion) - { - await EnrichWithNightlyInfo(updateDtos); - } - - // Find the latest dto - var latestRelease = updateDtos[0]!; - var updateVersion = new Version(latestRelease.UpdateVersion); - var isNightly = BuildInfo.Version > new Version(latestRelease.UpdateVersion); - - // isNightly can be true when we compare something like v0.8.1 vs v0.8.1.0 - if (IsVersionEqualToBuildVersion(updateVersion)) - { - isNightly = false; - } - - - latestRelease.IsOnNightlyInRelease = isNightly; - - // Cache the fetched data - if (updateDtos.Count > 0) - { - await CacheReleasesAsync(updateDtos); - } - - if (count > 0) - { - return updateDtos.Take(count).ToList(); - } - - return updateDtos; - } - - /// - /// Compares 2 versions and ensures that the minor is always there - /// - /// - /// - /// - private static bool IsVersionEqual(string v1, string v2) - { - var versionParts = v1.Split('.'); - if (versionParts.Length < 4) - { - v1 += ".0"; // Append missing parts - } - - versionParts = v2.Split('.'); - if (versionParts.Length < 4) - { - v2 += ".0"; // Append missing parts - } - - return string.Equals(v2, v2, StringComparison.OrdinalIgnoreCase); - } - - private async Task?> TryGetCachedReleases() - { - if (!File.Exists(_cacheFilePath)) return null; - - var fileInfo = new FileInfo(_cacheFilePath); - if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) - { - var cachedData = await File.ReadAllTextAsync(_cacheFilePath); - return JsonSerializer.Deserialize>(cachedData); - } - - return null; - } - - private async Task TryGetCachedLatestRelease() - { - if (!File.Exists(_cacheLatestReleaseFilePath)) return null; - - var fileInfo = new FileInfo(_cacheLatestReleaseFilePath); - if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) - { - var cachedData = await File.ReadAllTextAsync(_cacheLatestReleaseFilePath); - return JsonSerializer.Deserialize(cachedData); - } - - return null; - } - - private async Task CacheReleasesAsync(IList updates) - { - try - { - var json = JsonSerializer.Serialize(updates, JsonOptions); - await File.WriteAllTextAsync(_cacheFilePath, json); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to cache releases"); - } - } - - private async Task CacheLatestReleaseAsync(UpdateNotificationDto update) - { - try - { - var json = JsonSerializer.Serialize(update, JsonOptions); - await File.WriteAllTextAsync(_cacheLatestReleaseFilePath, json); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to cache latest release"); - } - } - - - - private static bool IsVersionEqualToBuildVersion(Version updateVersion) - { - return updateVersion == BuildInfo.Version || (updateVersion.Revision < 0 && BuildInfo.Version.Revision == 0 && - BuildInfo.Version.CompareWithoutRevision(updateVersion)); - } - - - /// - /// Returns the number of releases ahead of this install version. If this install version is on a nightly, - /// then include nightly releases, otherwise only count Stable releases. - /// - /// Only count Stable releases - /// - public async Task GetNumberOfReleasesBehind(bool stableOnly = false) - { - var updates = await GetAllReleases(); - - // If the user is on nightly, then we need to handle releases behind differently - if (!stableOnly && (updates[0].IsPrerelease || updates[0].IsOnNightlyInRelease)) - { - return updates.Count(u => u.IsReleaseNewer); - } - - return updates - .Where(update => !update.IsPrerelease) - .Count(u => u.IsReleaseNewer); - } - - /// - /// Clears the Github cache - /// - public void BustGithubCache() - { - try - { - File.Delete(_cacheFilePath); - File.Delete(_cacheLatestReleaseFilePath); - } catch (Exception ex) - { - _logger.LogError(ex, "Failed to clear Github cache"); - } + return updates.Select(CreateDto).Where(d => d != null)!; } private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update) @@ -468,33 +92,15 @@ public partial class VersionUpdaterService : IVersionUpdaterService var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty)); var currentVersion = BuildInfo.Version.ToString(4); - var bodyHtml = _markdown.Transform(update.Body.Trim()); - var parsedSections = ParseReleaseBody(update.Body); - var blogPart = _markdown.Transform(ExtractBlogPart(update.Body).Trim()); - return new UpdateNotificationDto() { CurrentVersion = currentVersion, UpdateVersion = updateVersion.ToString(), - UpdateBody = bodyHtml, + UpdateBody = _markdown.Transform(update.Body.Trim()), UpdateTitle = update.Name, UpdateUrl = update.Html_Url, IsDocker = OsInfo.IsDocker, - PublishDate = update.Published_At, - IsReleaseEqual = IsVersionEqualToBuildVersion(updateVersion), - IsReleaseNewer = BuildInfo.Version < updateVersion, - IsPrerelease = false, - - Added = parsedSections.TryGetValue("Added", out var added) ? added : [], - Removed = parsedSections.TryGetValue("Removed", out var removed) ? removed : [], - Changed = parsedSections.TryGetValue("Changed", out var changed) ? changed : [], - Fixed = parsedSections.TryGetValue("Fixed", out var fixes) ? fixes : [], - Theme = parsedSections.TryGetValue("Theme", out var theme) ? theme : [], - Developer = parsedSections.TryGetValue("Developer", out var developer) ? developer : [], - KnownIssues = parsedSections.TryGetValue("Known Issues", out var knownIssues) ? knownIssues : [], - Api = parsedSections.TryGetValue("Api", out var api) ? api : [], - FeatureRequests = parsedSections.TryGetValue("Feature Requests", out var frs) ? frs : [], - BlogPart = blogPart + PublishDate = update.Published_At }; } @@ -503,7 +109,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { if (update == null) return; - var updateVersion = new Version(update.UpdateVersion); + var updateVersion = new Version(update.CurrentVersion); if (BuildInfo.Version < updateVersion) { @@ -511,29 +117,15 @@ public partial class VersionUpdaterService : IVersionUpdaterService await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), true); } - } - - private async Task GetVersionFromCommit(string commitSha) - { - try + else if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) { - // Use the raw GitHub URL format for the csproj file - var content = await $"https://raw.githubusercontent.com/Kareadita/Kavita/{commitSha}/Kavita.Common/Kavita.Common.csproj" - .WithHeader("User-Agent", "Kavita") - .GetStringAsync(); - - var versionMatch = Regex.Match(content, @"([0-9\.]+)"); - return versionMatch.Success ? versionMatch.Groups[1].Value : null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get version from commit {Sha}: {Message}", commitSha, ex.Message); - return null; + _logger.LogWarning("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version); + await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), + true); } } - private static async Task GetGithubRelease() { var update = await GithubLatestReleasesUrl @@ -544,109 +136,13 @@ public partial class VersionUpdaterService : IVersionUpdaterService return update; } - private static async Task> GetGithubReleases() + private static async Task> GetGithubReleases() { var update = await GithubAllReleasesUrl .WithHeader("Accept", "application/json") .WithHeader("User-Agent", "Kavita") - .GetJsonAsync>(); + .GetJsonAsync>(); return update; } - - private static string ExtractBlogPart(string body) - { - if (body.StartsWith('#')) return string.Empty; - var match = BlogPartRegex().Match(body); - return match.Success ? match.Groups[1].Value.Trim() : body.Trim(); - } - - private static Dictionary> ParseReleaseBody(string body) - { - var sections = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var lines = body.Split('\n'); - string? currentSection = null; - - foreach (var line in lines) - { - var trimmedLine = line.Trim(); - - // Check for section headers (case-insensitive) - if (trimmedLine.StartsWith('#')) - { - currentSection = trimmedLine.TrimStart('#').Trim(); - sections[currentSection] = []; - continue; - } - - // Parse items under a section - if (currentSection != null && - trimmedLine.StartsWith("- ") && - !string.IsNullOrWhiteSpace(trimmedLine)) - { - // Remove "Fixed:", "Added:" etc. if present - var cleanedItem = CleanSectionItem(trimmedLine); - - // Some sections like API/Developer/Removed don't have the title repeated, so we need to check for an additional cleaning - if (cleanedItem.StartsWith("- ")) - { - cleanedItem = trimmedLine.Substring(2); - } - - // Only add non-empty items - if (!string.IsNullOrWhiteSpace(cleanedItem)) - { - sections[currentSection].Add(cleanedItem); - } - } - } - - return sections; - } - - private static string CleanSectionItem(string item) - { - // Remove everything up to and including the first ":" - var colonIndex = item.IndexOf(':'); - if (colonIndex != -1) - { - item = item.Substring(colonIndex + 1).Trim(); - } - - return item; - } - - private sealed class PullRequestInfo - { - public required string Title { get; init; } - public required string Body { get; init; } - public required string Html_Url { get; init; } - public required string Merged_At { get; init; } - public required int Number { get; init; } - } - - private sealed class CommitInfo - { - public required string Sha { get; init; } - public required CommitDetail Commit { get; init; } - public required string Html_Url { get; init; } - } - - private sealed class CommitDetail - { - public required string Message { get; init; } - public required CommitAuthor Author { get; init; } - } - - private sealed class CommitAuthor - { - public required string Date { get; init; } - } - - private sealed class NightlyInfo - { - public required string Version { get; init; } - public required int PrNumber { get; init; } - public required DateTime Date { get; init; } - } } diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 720d97663..b6b0ade47 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -4,12 +4,10 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.DTOs.Account; using API.Entities; -using API.Helpers; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -19,14 +17,14 @@ using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegiste namespace API.Services; -#nullable enable public interface ITokenService { Task CreateToken(AppUser user); Task ValidateRefreshToken(TokenRequestDto request); Task CreateRefreshToken(AppUser user); - Task GetJwtFromUser(AppUser user); + Task GetJwtFromUser(AppUser user); + bool HasTokenExpired(string token); } @@ -37,7 +35,6 @@ public class TokenService : ITokenService private readonly IUnitOfWork _unitOfWork; private readonly SymmetricSecurityKey _key; private const string RefreshTokenName = "RefreshToken"; - private static readonly SemaphoreSlim _refreshTokenLock = new SemaphoreSlim(1, 1); public TokenService(IConfiguration config, UserManager userManager, ILogger logger, IUnitOfWork unitOfWork) { @@ -83,8 +80,6 @@ public class TokenService : ITokenService public async Task ValidateRefreshToken(TokenRequestDto request) { - await _refreshTokenLock.WaitAsync(); - try { var tokenHandler = new JwtSecurityTokenHandler(); @@ -95,7 +90,6 @@ public class TokenService : ITokenService _logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken"); return null; } - var user = await _userManager.FindByNameAsync(username); if (user == null) { @@ -103,19 +97,13 @@ public class TokenService : ITokenService return null; } - var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, - RefreshTokenName, request.RefreshToken); + var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken); if (!validated && tokenContent.ValidTo <= DateTime.UtcNow.Add(TimeSpan.FromHours(1))) { _logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); return null; } - // Remove the old refresh token first - await _userManager.RemoveAuthenticationTokenAsync(user, - TokenOptions.DefaultProvider, - RefreshTokenName); - try { user.UpdateLastActive(); @@ -132,8 +120,7 @@ public class TokenService : ITokenService Token = await CreateToken(user), RefreshToken = await CreateRefreshToken(user) }; - } - catch (SecurityTokenExpiredException ex) + } catch (SecurityTokenExpiredException ex) { // Handle expired token _logger.LogError(ex, "Failed to validate refresh token"); @@ -145,27 +132,19 @@ public class TokenService : ITokenService _logger.LogError(ex, "Failed to validate refresh token"); return null; } - finally - { - _refreshTokenLock.Release(); - } } - public async Task GetJwtFromUser(AppUser user) + public async Task GetJwtFromUser(AppUser user) { var userClaims = await _userManager.GetClaimsAsync(user); var jwtClaim = userClaims.FirstOrDefault(claim => claim.Type == "jwt"); return jwtClaim?.Value; } - public static bool HasTokenExpired(string? token) + public bool HasTokenExpired(string token) { - return !JwtHelper.IsTokenValid(token); - } - - - public static DateTime GetTokenExpiry(string? token) - { - return JwtHelper.GetTokenExpiry(token); + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenContent = tokenHandler.ReadJwtToken(token); + return tokenContent.ValidTo <= DateTime.UtcNow; } } diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs index 25bf5a819..fcdc17b14 100644 --- a/API/SignalR/EventHub.cs +++ b/API/SignalR/EventHub.cs @@ -21,11 +21,13 @@ public class EventHub : IEventHub { private readonly IHubContext _messageHub; private readonly IPresenceTracker _presenceTracker; + private readonly IUnitOfWork _unitOfWork; - public EventHub(IHubContext messageHub, IPresenceTracker presenceTracker) + public EventHub(IHubContext messageHub, IPresenceTracker presenceTracker, IUnitOfWork unitOfWork) { _messageHub = messageHub; _presenceTracker = presenceTracker; + _unitOfWork = unitOfWork; // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) } diff --git a/API/SignalR/LogHub.cs b/API/SignalR/LogHub.cs index c52121b7d..975711dfd 100644 --- a/API/SignalR/LogHub.cs +++ b/API/SignalR/LogHub.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace API.SignalR; -#nullable enable public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub { diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 87a464e6a..76fcae5fc 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -1,6 +1,5 @@ using System; using API.DTOs.Update; -using API.Entities.Person; using API.Extensions; using API.Services.Plus; @@ -14,7 +13,6 @@ public static class MessageFactoryEntityTypes public const string Chapter = "chapter"; public const string CollectionTag = "collection"; public const string ReadingList = "readingList"; - public const string Person = "person"; } public static class MessageFactory { @@ -43,9 +41,9 @@ public static class MessageFactory ///
public const string OnlineUsers = "OnlineUsers"; /// - /// When a Collection has been updated + /// When a series is added to a collection /// - public const string CollectionUpdated = "CollectionUpdated"; + public const string SeriesAddedToCollection = "SeriesAddedToCollection"; /// /// Event sent out during backing up the database /// @@ -132,30 +130,6 @@ public static class MessageFactory /// Order, Visibility, etc has changed on the Sidenav. UI will refresh the layout /// public const string SideNavUpdate = "SideNavUpdate"; - /// - /// A Theme was updated and UI should refresh to get the latest version - /// - public const string SiteThemeUpdated = "SiteThemeUpdated"; - /// - /// A Progress event when a smart collection is synchronizing - /// - public const string SmartCollectionSync = "SmartCollectionSync"; - /// - /// Chapter is removed from server - /// - public const string ChapterRemoved = "ChapterRemoved"; - /// - /// Volume is removed from server - /// - public const string VolumeRemoved = "VolumeRemoved"; - /// - /// A Person merged has been merged into another - /// - public const string PersonMerged = "PersonMerged"; - /// - /// A Rate limit error was hit when matching a series with Kavita+ - /// - public const string ExternalMatchRateLimitError = "ExternalMatchRateLimitError"; public static SignalRMessage DashboardUpdateEvent(int userId) { @@ -231,32 +205,6 @@ public static class MessageFactory }; } - public static SignalRMessage ChapterRemovedEvent(int chapterId, int seriesId) - { - return new SignalRMessage() - { - Name = ChapterRemoved, - Body = new - { - SeriesId = seriesId, - ChapterId = chapterId - } - }; - } - - public static SignalRMessage VolumeRemovedEvent(int volumeId, int seriesId) - { - return new SignalRMessage() - { - Name = VolumeRemoved, - Body = new - { - SeriesId = seriesId, - VolumeId = volumeId - } - }; - } - public static SignalRMessage WordCountAnalyzerProgressEvent(int libraryId, float progress, string eventType, string subtitle = "") { @@ -362,17 +310,17 @@ public static class MessageFactory }; } - - public static SignalRMessage CollectionUpdatedEvent(int collectionId) + public static SignalRMessage SeriesAddedToCollectionEvent(int tagId, int seriesId) { return new SignalRMessage { - Name = CollectionUpdated, + Name = SeriesAddedToCollection, Progress = ProgressType.None, EventType = ProgressEventType.Single, Body = new { - TagId = collectionId, + TagId = tagId, + SeriesId = seriesId } }; } @@ -389,7 +337,6 @@ public static class MessageFactory EventType = ProgressEventType.Single, Body = new { - Name = Error, Title = title, SubTitle = subtitle, } @@ -407,7 +354,6 @@ public static class MessageFactory EventType = ProgressEventType.Single, Body = new { - Name = Info, Title = title, SubTitle = subtitle, } @@ -430,13 +376,13 @@ public static class MessageFactory }; } - public static SignalRMessage DownloadProgressEvent(string username, string downloadName, string subtitle, float progress, string eventType = "updated") + public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated") { return new SignalRMessage() { Name = DownloadProgress, - Title = $"Preparing {username.SentenceCase()} the download of {downloadName}", - SubTitle = subtitle, + Title = $"Downloading {downloadName}", + SubTitle = $"Preparing {username.SentenceCase()} the download of {downloadName}", EventType = eventType, Progress = ProgressType.Determinate, Body = new @@ -475,31 +421,6 @@ public static class MessageFactory }; } - /// - /// Represents a file being scanned by Kavita for processing and grouping - /// - /// Does not have a progress as it's unknown how many files there are. Instead sends -1 to represent indeterminate - /// - /// - /// - /// - public static SignalRMessage SmartCollectionProgressEvent(string collectionName, string seriesName, int currentItems, int totalItems, string eventType) - { - return new SignalRMessage() - { - Name = SmartCollectionSync, - Title = $"Synchronizing {collectionName}", - SubTitle = seriesName, - EventType = eventType, - Progress = ProgressType.Determinate, - Body = new - { - Progress = float.Min((currentItems / (totalItems * 1.0f)), 100f), - EventTime = DateTime.Now - } - }; - } - /// /// This informs the UI with details about what is being processed by the Scanner /// @@ -507,7 +428,7 @@ public static class MessageFactory /// /// /// - public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "", int? totalToProcess = null) + public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "") { return new SignalRMessage() { @@ -516,12 +437,7 @@ public static class MessageFactory SubTitle = seriesName, EventType = eventType, Progress = ProgressType.Indeterminate, - Body = new - { - SeriesName = seriesName, - LibraryName = libraryName, - LeftToProcess = totalToProcess - } + Body = null }; } @@ -564,7 +480,7 @@ public static class MessageFactory return new SignalRMessage() { Name = SiteThemeProgress, - Title = "Processing Site Theme", // TODO: Localize SignalRMessage titles + Title = "Scanning Site Theme", SubTitle = subtitle, EventType = eventType, Progress = ProgressType.Indeterminate, @@ -575,25 +491,6 @@ public static class MessageFactory }; } - /// - /// Sends an event to the UI informing of a SiteTheme update and UI needs to refresh the content - /// - /// - /// - public static SignalRMessage SiteThemeUpdatedEvent(string themeName) - { - return new SignalRMessage() - { - Name = SiteThemeUpdated, - Title = "SiteTheme Update", - Progress = ProgressType.None, - Body = new - { - ThemeName = themeName, - } - }; - } - public static SignalRMessage BookThemeProgressEvent(string subtitle, string themeName, string eventType) { return new SignalRMessage() @@ -670,29 +567,4 @@ public static class MessageFactory EventType = ProgressEventType.Single, }; } - - public static SignalRMessage PersonMergedMessage(Person dst, Person src) - { - return new SignalRMessage() - { - Name = PersonMerged, - Body = new - { - srcId = src.Id, - dstName = dst.Name, - }, - }; - } - public static SignalRMessage ExternalMatchRateLimitErrorEvent(int seriesId, string seriesName) - { - return new SignalRMessage() - { - Name = ExternalMatchRateLimitError, - Body = new - { - seriesId = seriesId, - seriesName = seriesName, - }, - }; - } } diff --git a/API/SignalR/MessageHub.cs b/API/SignalR/MessageHub.cs index d06d27832..85c1467fb 100644 --- a/API/SignalR/MessageHub.cs +++ b/API/SignalR/MessageHub.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace API.SignalR; -#nullable enable /// /// Generic hub for sending messages to UI diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index 600a4197a..87d748841 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using API.Data; namespace API.SignalR.Presence; -#nullable enable public interface IPresenceTracker { @@ -23,6 +22,7 @@ internal class ConnectionDetail public bool IsAdmin { get; set; } } +// TODO: This can respond to UserRoleUpdate events to handle online users /// /// This is a singleton service for tracking what users have a SignalR connection and their difference connectionIds /// diff --git a/API/SignalR/SignalRMessage.cs b/API/SignalR/SignalRMessage.cs index f00a677b9..d3f250293 100644 --- a/API/SignalR/SignalRMessage.cs +++ b/API/SignalR/SignalRMessage.cs @@ -1,7 +1,6 @@ using System; namespace API.SignalR; -#nullable enable /// /// Payload for SignalR messages to Frontend diff --git a/API/Startup.cs b/API/Startup.cs index f57cb7d01..4418fe270 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -41,7 +41,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Serilog; -using Swashbuckle.AspNetCore.SwaggerGen; using TaskScheduler = API.Services.TaskScheduler; namespace API; @@ -55,9 +54,6 @@ public class Startup { _config = config; _env = env; - - // Disable Hangfire Automatic Retry - GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute { Attempts = 0 }); } // This method gets called by the runtime. Use this method to add services to the container. @@ -142,13 +138,13 @@ public class Startup c.SwaggerDoc("v1", new OpenApiInfo { Version = BuildInfo.Version.ToString(), - Title = $"Kavita", - Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version}", + Title = "Kavita", + Description = "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage.", License = new OpenApiLicense { Name = "GPL-3.0", Url = new Uri("https://github.com/Kareadita/Kavita/blob/develop/LICENSE") - }, + } }); var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; @@ -180,7 +176,7 @@ public class Startup Url = "{protocol}://{hostpath}", Variables = new Dictionary { - { "protocol", new OpenApiServerVariable { Default = "http", Enum = ["http", "https"]} }, + { "protocol", new OpenApiServerVariable { Default = "http", Enum = new List { "http", "https" } } }, { "hostpath", new OpenApiServerVariable { Default = "localhost:5000" } } } }); @@ -211,7 +207,7 @@ public class Startup .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseInMemoryStorage()); - //.UseSQLiteStorage("config/Hangfire.db")); // UseSQLiteStorage - SQLite has some issues around resuming jobs when aborted (and locking can cause high utilization) (NOTE: There is code to clear jobs on startup a redditor gave me) + //.UseSQLiteStorage("config/Hangfire.db")); // UseSQLiteStorage - SQLite has some issues around resuming jobs when aborted (and locking can cause high utilization) // Add the processing server as IHostedService services.AddHangfireServer(options => @@ -226,98 +222,38 @@ public class Startup // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService, - IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService, IVersionUpdaterService versionService) + IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService) { - var logger = serviceProvider.GetRequiredService>(); // Apply Migrations try { Task.Run(async () => { // Apply all migrations on startup + var logger = serviceProvider.GetRequiredService>(); + var userManager = serviceProvider.GetRequiredService>(); var dataContext = serviceProvider.GetRequiredService(); - logger.LogInformation("Running Migrations"); - #region Migrations + logger.LogInformation("Running Migrations"); // v0.7.9 await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); - // v0.7.11 - await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); - await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger); - - - // v0.7.14 - await MigrateEmailTemplates.Migrate(directoryService, logger); - await MigrateVolumeNumber.Migrate(dataContext, logger); - await MigrateWantToReadImport.Migrate(unitOfWork, dataContext, directoryService, logger); - await MigrateManualHistory.Migrate(dataContext, logger); - await MigrateClearNightlyExternalSeriesRecords.Migrate(dataContext, logger); - - // v0.8.0 - await MigrateVolumeLookupName.Migrate(dataContext, unitOfWork, logger); - await MigrateChapterNumber.Migrate(dataContext, logger); - await MigrateProgressExport.Migrate(dataContext, directoryService, logger); - await MigrateMixedSpecials.Migrate(dataContext, unitOfWork, directoryService, logger); - await MigrateLooseLeafChapters.Migrate(dataContext, unitOfWork, directoryService, logger); - await MigrateChapterFields.Migrate(dataContext, unitOfWork, logger); - await MigrateChapterRange.Migrate(dataContext, unitOfWork, logger); - await MigrateMangaFilePath.Migrate(dataContext, logger); - await MigrateCollectionTagToUserCollections.Migrate(dataContext, unitOfWork, logger); - - // v0.8.1 - await MigrateLowestSeriesFolderPath.Migrate(dataContext, unitOfWork, logger); - - // v0.8.2 - await ManualMigrateThemeDescription.Migrate(dataContext, logger); - await MigrateInitialInstallData.Migrate(dataContext, logger, directoryService); - await MigrateSeriesLowestFolderPath.Migrate(dataContext, logger, directoryService); - - // v0.8.4 - await MigrateLowestSeriesFolderPath2.Migrate(dataContext, unitOfWork, logger); - await ManualMigrateRemovePeople.Migrate(dataContext, logger); - await MigrateDuplicateDarkTheme.Migrate(dataContext, logger); - await ManualMigrateUnscrobbleBookLibraries.Migrate(dataContext, logger); - - // v0.8.5 - await ManualMigrateBlacklistTableToSeries.Migrate(dataContext, logger); - await ManualMigrateInvalidBlacklistSeries.Migrate(dataContext, logger); - await ManualMigrateScrobbleErrors.Migrate(dataContext, logger); - await ManualMigrateNeedsManualMatch.Migrate(dataContext, logger); - await MigrateProgressExportForV085.Migrate(dataContext, directoryService, logger); - - // v0.8.6 - await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger); - await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger); - - // v0.8.7 - await ManualMigrateReadingProfiles.Migrate(dataContext, logger); - - #endregion - // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); - var isVersionDifferent = installVersion.Value != BuildInfo.Version.ToString(); installVersion.Value = BuildInfo.Version.ToString(); unitOfWork.SettingsRepository.Update(installVersion); + await unitOfWork.CommitAsync(); - logger.LogInformation("Running Migrations - complete"); - - if (isVersionDifferent) - { - // Clear the Github cache so update stuff shows correctly - versionService.BustGithubCache(); - } - }).GetAwaiter() .GetResult(); } catch (Exception ex) { + var logger = serviceProvider.GetRequiredService>(); logger.LogCritical(ex, "An error occurred during migration"); } @@ -395,14 +331,7 @@ public class Startup app.UseStaticFiles(new StaticFileOptions { - // bcmap files needed for PDF reader localizations (https://github.com/Kareadita/Kavita/issues/2970) - ContentTypeProvider = new FileExtensionContentTypeProvider - { - Mappings = - { - [".bcmap"] = "application/octet-stream" - } - }, + ContentTypeProvider = new FileExtensionContentTypeProvider(), HttpsCompression = HttpsCompressionMode.Compress, OnPrepareResponse = ctx => { @@ -415,7 +344,6 @@ public class Startup => { opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest; - opts.IncludeQueryInRequestPath = true; }); app.Use(async (context, next) => @@ -423,19 +351,11 @@ public class Startup context.Response.Headers[HeaderNames.Vary] = new[] { "Accept-Encoding" }; + // Don't let the site be iframed outside the same origin (clickjacking) + context.Response.Headers.XFrameOptions = Configuration.XFrameOptions; - if (!Configuration.AllowIFraming) - { - // Don't let the site be iframed outside the same origin (clickjacking) - context.Response.Headers.XFrameOptions = "SAMEORIGIN"; - - // Setup CSP to ensure we load assets only from these origins - context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); - } - else - { - logger.LogCritical("appsetting.json has allow iframing on! This may allow for clickjacking on the server. User beware"); - } + // Setup CSP to ensure we load assets only from these origins + context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); await next(); }); @@ -445,10 +365,7 @@ public class Startup endpoints.MapControllers(); endpoints.MapHub("hubs/messages"); endpoints.MapHub("hubs/logs"); - if (env.IsDevelopment()) - { - endpoints.MapHangfireDashboard(); - } + endpoints.MapHangfireDashboard(); endpoints.MapFallbackToController("Index", "Fallback"); }); @@ -457,16 +374,18 @@ public class Startup { try { + var logger = serviceProvider.GetRequiredService>(); logger.LogInformation("Kavita - v{Version}", BuildInfo.Version); } catch (Exception) { /* Swallow Exception */ - Console.WriteLine($"Kavita - v{BuildInfo.Version}"); } + Console.WriteLine($"Kavita - v{BuildInfo.Version}"); }); - logger.LogInformation("Starting with base url as {BaseUrl}", basePath); + var _logger = serviceProvider.GetRequiredService>(); + _logger.LogInformation("Starting with base url as {BaseUrl}", basePath); } private static void UpdateBaseUrlInIndex(string baseUrl) @@ -483,7 +402,9 @@ public class Startup } catch (Exception ex) { - if (ex is UnauthorizedAccessException && baseUrl.Equals(Configuration.DefaultBaseUrl) && OsInfo.IsDocker) + if ((ex.Message.Contains("Permission denied") + || ex.Message.Contains("UnauthorizedAccessException")) + && baseUrl.Equals(Configuration.DefaultBaseUrl) && OsInfo.IsDocker) { // Swallow the exception as the install is non-root and Docker return; diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index ad2d89fa5..90faa9e5f 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -1,8 +1,8 @@ { "TokenKey": "super secret unguessable key that is longer because we require it", "Port": 5000, - "IpAddresses": "", - "BaseUrl": "/", - "Cache": 75, - "AllowIFraming": false + "IpAddresses": "", + "BaseUrl": "/test/", + "Cache": 90, + "XFrameOrigins": "SAMEORIGIN" } \ No newline at end of file diff --git a/API/config/appsettings.json b/API/config/appsettings.json index c77ff6a30..3eeee1c18 100644 --- a/API/config/appsettings.json +++ b/API/config/appsettings.json @@ -3,5 +3,5 @@ "Port": 5000, "IpAddresses": "", "BaseUrl": "/", - "Cache": 75 + "Cache": 50 } diff --git a/API/config/templates/EmailChange.html b/API/config/templates/EmailChange.html deleted file mode 100644 index 7a960aea9..000000000 --- a/API/config/templates/EmailChange.html +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - Event - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
Your account's email has been updated on {{InvitingUser}}'s Kavita instance. Click the button to validate your email.
- - - -
- - - diff --git a/API/config/templates/EmailConfirm.html b/API/config/templates/EmailConfirm.html deleted file mode 100644 index 4aa4f701c..000000000 --- a/API/config/templates/EmailConfirm.html +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - Event - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
You have been invited to {{InvitingUser}}'s Kavita instance. Click the button to accept the invite.
- - - -
- - - diff --git a/API/config/templates/EmailMigration.html b/API/config/templates/EmailMigration.html deleted file mode 100644 index 553635070..000000000 --- a/API/config/templates/EmailMigration.html +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - Kavita - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
Email confirmation is required for continued access. Click the button to confirm your email.
- - - -
- - - diff --git a/API/config/templates/EmailPasswordReset.html b/API/config/templates/EmailPasswordReset.html deleted file mode 100644 index 7ac7dc315..000000000 --- a/API/config/templates/EmailPasswordReset.html +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - Kavita - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
{{Preheader}}
- - - -
- - - diff --git a/API/config/templates/EmailTest.html b/API/config/templates/EmailTest.html deleted file mode 100644 index 358f4f8e1..000000000 --- a/API/config/templates/EmailTest.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - - - - - - - Event - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
- This is a Test Email -
- - - -
- - - diff --git a/API/config/templates/SendToDevice.html b/API/config/templates/SendToDevice.html deleted file mode 100644 index aab35cf68..000000000 --- a/API/config/templates/SendToDevice.html +++ /dev/null @@ -1,323 +0,0 @@ - - - - - - - - - - - - - Event - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
You've been sent a file from Kavita!
- - - -
- - - diff --git a/API/config/templates/TokenExpiration.html b/API/config/templates/TokenExpiration.html deleted file mode 100644 index 4780ec010..000000000 --- a/API/config/templates/TokenExpiration.html +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - - - - Kavita - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
{{Preheader}}
- - - -
- - - diff --git a/API/config/templates/TokenExpiringSoon.html b/API/config/templates/TokenExpiringSoon.html deleted file mode 100644 index eac990260..000000000 --- a/API/config/templates/TokenExpiringSoon.html +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - - - - Kavita - [Plain HTML] - - - - - - - - - - - - - - - - - - - - - - -
- -
{{Preheader}}
- - - -
- - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 292217862..238e54542 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,36 +3,32 @@ We're always looking for people to help make Kavita even better, there are a number of ways to contribute. ## Documentation ## -Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavitareader.com/contributing) the better. +Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavitareader.com/) the better. ## Development ## ### Tools required ### - Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works fine. [Download it here](https://www.visualstudio.com/downloads/). -- Rider (optional to Visual Studio, preferred editor) (https://www.jetbrains.com/rider/) +- Rider (optional to Visual Studio) (https://www.jetbrains.com/rider/) - HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc) - [Git](https://git-scm.com/downloads) -- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher) -- .NET 9.0+ -- dotnet tool install -g Swashbuckle.AspNetCore.Cli +- [NodeJS](https://nodejs.org/en/download/) (Node 16.X.X or higher) +- .NET 7.0+ +- dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli ### Getting started ### 1. Fork Kavita 2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github) 3. Install the required Node Packages - - `cd Kavita/UI/Web` + - cd Kavita/UI/Web - `npm install` - `npm install -g @angular/cli` -5. Start the frontend - - `npm run start` -6. Build the project in Visual Studio/Rider, Setting startup project to `API` -7. Debug the project in Visual Studio/Rider -8. Open http://localhost:4200 -9. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs. - -### Debugging on Device ### -- Update `IP` constant in `Web/UI/src/environments/environment.ts` to your dev machine's ip instead of `localhost`. +4. Start angular server `ng serve` +5. Build the project in Visual Studio/Rider, Setting startup project to `API` +6. Debug the project in Visual Studio/Rider +7. Open http://localhost:4200 +8. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs. ### Contributing Code ### @@ -65,7 +61,4 @@ If you just want to play with Swagger, you can just - dotnet run -c Debug - Go to http://localhost:5000/swagger/index.html -If you have a build issue around swagger run: -` swagger tofile --output ../openapi.json API/bin/Debug/net8.0/API.dll v1` to see the error and correct it - If you have any questions about any of this, please let us know. diff --git a/Dockerfile b/Dockerfile index bfc253c0e..0b4174d09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,8 @@ ARG TARGETPLATFORM #Move the output files to where they need to be RUN mkdir /files COPY _output/*.tar.gz /files/ -COPY UI/Web/dist/browser /files/wwwroot +COPY UI/Web/dist /files/wwwroot COPY copy_runtime.sh /copy_runtime.sh - -RUN chmod +x /copy_runtime.sh RUN /copy_runtime.sh RUN chmod +x /Kavita/Kavita @@ -34,9 +32,8 @@ EXPOSE 5000 WORKDIR /kavita -HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 CMD curl -fsS http://localhost:5000/api/health || exit 1 +HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000/api/health || exit 1 -# Enable detection of running in a container ENV DOTNET_RUNNING_IN_CONTAINER=true ENTRYPOINT [ "/bin/bash" ] diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index ba4fd09b7..e2af4c32d 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -13,12 +13,11 @@ public static class Configuration public const string DefaultBaseUrl = "/"; public const int DefaultHttpPort = 5000; public const int DefaultTimeOutSecs = 90; + public const string DefaultXFrameOptions = "SAMEORIGIN"; public const long DefaultCacheMemory = 75; private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); - public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "https://plus.kavitareader.com" : "https://plus.kavitareader.com"; // http://localhost:5020 - public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; + public static string KavitaPlusApiUrl = "https://plus.kavitareader.com"; public static int Port { @@ -50,7 +49,7 @@ public static class Configuration set => SetCacheSize(GetAppSettingFilename(), value); } - public static bool AllowIFraming => GetAllowIFraming(GetAppSettingFilename()); + public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename()); private static string GetAppSettingFilename() { @@ -294,21 +293,26 @@ public static class Configuration #endregion - #region AllowIFraming - private static bool GetAllowIFraming(string filePath) + #region XFrameOrigins + private static string GetXFrameOptions(string filePath) { + if (OsInfo.IsDocker) + { + return DefaultBaseUrl; + } + try { var json = File.ReadAllText(filePath); var jsonObj = JsonSerializer.Deserialize(json); - return jsonObj.AllowIFraming; + return !string.IsNullOrEmpty(jsonObj.XFrameOrigins) ? jsonObj.XFrameOrigins : DefaultXFrameOptions; } catch (Exception ex) { Console.WriteLine("Error reading app settings: " + ex.Message); } - return false; + return DefaultXFrameOptions; } #endregion @@ -316,7 +320,6 @@ public static class Configuration { public string TokenKey { get; set; } // ReSharper disable once MemberHidesStaticFromOuterClass -#pragma warning disable S3218 public int Port { get; set; } = DefaultHttpPort; // ReSharper disable once MemberHidesStaticFromOuterClass public string IpAddresses { get; set; } = string.Empty; @@ -325,7 +328,6 @@ public static class Configuration // ReSharper disable once MemberHidesStaticFromOuterClass public long Cache { get; set; } = DefaultCacheMemory; // ReSharper disable once MemberHidesStaticFromOuterClass - public bool AllowIFraming { get; init; } = false; -#pragma warning restore S3218 + public string XFrameOrigins { get; set; } = DefaultXFrameOptions; } } diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index d8cc6a070..1f851be00 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -10,9 +10,7 @@ public static class OsInfo public static bool IsLinux => Os is Os.Linux or Os.LinuxMusl or Os.Bsd; public static bool IsOsx => Os == Os.Osx; public static bool IsWindows => Os == Os.Windows; - public static bool IsDocker => - Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" || - Environment.GetEnvironmentVariable("LSIO_FIRST_PARTY") == "true"; + public static bool IsDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; static OsInfo() { diff --git a/Kavita.Common/Helpers/CronHelper.cs b/Kavita.Common/Helpers/CronHelper.cs deleted file mode 100644 index 0b40113ce..000000000 --- a/Kavita.Common/Helpers/CronHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Cronos; - -namespace Kavita.Common.Helpers; - -public static class CronHelper -{ - public static bool IsValidCron(string cronExpression) - { - // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique - try - { - CronExpression.Parse(cronExpression); - return true; - } - catch (Exception) - { - /* Swallow */ - return false; - } - } -} diff --git a/Kavita.Common/Helpers/FlurlConfiguration.cs b/Kavita.Common/Helpers/FlurlConfiguration.cs deleted file mode 100644 index b80dff8d9..000000000 --- a/Kavita.Common/Helpers/FlurlConfiguration.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Flurl.Http; - -namespace Kavita.Common.Helpers; - -/// -/// Helper class for configuring Flurl client for a specific URL. -/// -public static class FlurlConfiguration -{ - private static readonly List ConfiguredClients = new List(); - private static readonly Lock Lock = new Lock(); - - /// - /// Configures the Flurl client for the specified URL. - /// - /// The URL to configure the client for. - public static void ConfigureClientForUrl(string url) - { - //Important client are mapped without path, per example two urls pointing to the same host:port but different path, will use the same client. - lock (Lock) - { - var ur = new Uri(url); - //key is host:port - var host = ur.Host + ":" + ur.Port; - if (ConfiguredClients.Contains(host)) return; - - FlurlHttp.ConfigureClientForUrl(url).ConfigureInnerHandler(cli => -#pragma warning disable S4830 - cli.ServerCertificateCustomValidationCallback = (_, _, _, _) => true); -#pragma warning restore S4830 - - ConfiguredClients.Add(host); - } - } -} diff --git a/Kavita.Common/Helpers/UntrustedCertClientFactory.cs b/Kavita.Common/Helpers/UntrustedCertClientFactory.cs new file mode 100644 index 000000000..6ddb2a9f3 --- /dev/null +++ b/Kavita.Common/Helpers/UntrustedCertClientFactory.cs @@ -0,0 +1,13 @@ +using System.Net.Http; +using Flurl.Http.Configuration; + +namespace Kavita.Common.Helpers; + +public class UntrustedCertClientFactory : DefaultHttpClientFactory +{ + public override HttpMessageHandler CreateMessageHandler() { + return new HttpClientHandler { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + } +} diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index c7dd0ab94..dc5e64464 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -1,23 +1,24 @@ + - net9.0 + net7.0 kavitareader.com Kavita - 0.8.7.1 + 0.7.10.0 en true - - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - \ No newline at end of file + + + diff --git a/Kavita.Common/KavitaException.cs b/Kavita.Common/KavitaException.cs index de10d3382..b624e0111 100644 --- a/Kavita.Common/KavitaException.cs +++ b/Kavita.Common/KavitaException.cs @@ -6,6 +6,7 @@ namespace Kavita.Common; /// /// These are used for errors to send to the UI that should not be reported to Sentry /// +[Serializable] public class KavitaException : Exception { public KavitaException() @@ -16,4 +17,8 @@ public class KavitaException : Exception public KavitaException(string message, Exception inner) : base(message, inner) { } + + protected KavitaException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/Kavita.Common/KavitaUnauthenticatedUserException.cs b/Kavita.Common/KavitaUnauthenticatedUserException.cs index ede20b59d..6cce9f981 100644 --- a/Kavita.Common/KavitaUnauthenticatedUserException.cs +++ b/Kavita.Common/KavitaUnauthenticatedUserException.cs @@ -7,6 +7,7 @@ namespace Kavita.Common; /// The user does not exist (aka unauthorized). This will be caught by middleware and Unauthorized() returned to UI ///
/// This will always log to Security Log +[Serializable] public class KavitaUnauthenticatedUserException : Exception { public KavitaUnauthenticatedUserException() @@ -17,4 +18,8 @@ public class KavitaUnauthenticatedUserException : Exception public KavitaUnauthenticatedUserException(string message, Exception inner) : base(message, inner) { } + + protected KavitaUnauthenticatedUserException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/Kavita.Common/RateLimitException.cs b/Kavita.Common/RateLimitException.cs deleted file mode 100644 index ecc0730f7..000000000 --- a/Kavita.Common/RateLimitException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Kavita.Common; - -/// -/// When a rate limit is hit -/// -public class RateLimitException : Exception -{ - public RateLimitException() - { } - - public RateLimitException(string message) : base(message) - { } - - public RateLimitException(string message, Exception inner) - : base(message, inner) { } -} diff --git a/Kavita.Email/DTOs/ConfirmationEmailDto.cs b/Kavita.Email/DTOs/ConfirmationEmailDto.cs deleted file mode 100644 index d157b4d53..000000000 --- a/Kavita.Email/DTOs/ConfirmationEmailDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Skeleton.DTOs; - -public record ConfirmationEmailDto -{ - public string InvitingUser { get; init; } - public string EmailAddress { get; init; } - public string ServerConfirmationLink { get; init; } - public string InstallId { get; init; } - -} \ No newline at end of file diff --git a/Kavita.Email/DTOs/EmailMigrationDto.cs b/Kavita.Email/DTOs/EmailMigrationDto.cs deleted file mode 100644 index dc210dbdb..000000000 --- a/Kavita.Email/DTOs/EmailMigrationDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Skeleton.DTOs; - -public class EmailMigrationDto -{ - public string EmailAddress { get; init; } - public string ServerConfirmationLink { get; init; } - public string Username { get; init; } - public string InstallId { get; init; } -} \ No newline at end of file diff --git a/Kavita.Email/DTOs/EmailOptionsDto.cs b/Kavita.Email/DTOs/EmailOptionsDto.cs deleted file mode 100644 index 242e618ee..000000000 --- a/Kavita.Email/DTOs/EmailOptionsDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Skeleton.DTOs; - -public class EmailOptionsDto -{ - public IList ToEmails { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - public IList> PlaceHolders { get; set; } - /// - /// Filenames to attach - /// - public IList Attachments { get; set; } -} \ No newline at end of file diff --git a/Kavita.Email/DTOs/PasswordResetDto.cs b/Kavita.Email/DTOs/PasswordResetDto.cs deleted file mode 100644 index 901eaa79c..000000000 --- a/Kavita.Email/DTOs/PasswordResetDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Skeleton.DTOs; - -public class PasswordResetDto -{ - public string EmailAddress { get; init; } - public string ServerConfirmationLink { get; init; } - public string InstallId { get; init; } -} \ No newline at end of file diff --git a/Kavita.Email/Kavita.Email.csproj b/Kavita.Email/Kavita.Email.csproj index 3a6353295..5a9557890 100644 --- a/Kavita.Email/Kavita.Email.csproj +++ b/Kavita.Email/Kavita.Email.csproj @@ -1,9 +1,22 @@ - + - net8.0 + net6.0 enable enable + + + + + + + + + + + + + diff --git a/Kavita.sln.DotSettings b/Kavita.sln.DotSettings index b46c328cd..92adaa72f 100644 --- a/Kavita.sln.DotSettings +++ b/Kavita.sln.DotSettings @@ -2,13 +2,9 @@ ExplicitlyExcluded True True - True - True - True True True True - True True True True diff --git a/README.md b/README.md index ffff8d831..f6d2a437e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # []() Kavita
-![new_github_preview_stills](https://github.com/user-attachments/assets/f016b34f-3c4c-4f07-8e72-12cd6f4e71ea) +![new_github_preview_stills](https://user-images.githubusercontent.com/735851/169657008-37812c18-5490-4e2a-9dcb-4806f8c87c69.gif) -Kavita is a fast, feature rich, cross-platform reading server. Built with a focus for being a full solution for all your reading needs. Set up your own server and share +Kavita is a fast, feature rich, cross platform reading server. Built with a focus for being a full solution for all your reading needs. Setup your own server and share your reading collection with your friends and family! [![Release](https://img.shields.io/github/release/Kareadita/Kavita.svg?style=flat&maxAge=3600)](https://github.com/Kareadita/Kavita/releases) [![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](https://github.com/Kareadita/Kavita/blob/master/LICENSE) [![Downloads](https://img.shields.io/github/downloads/Kareadita/Kavita/total.svg?style=flat)](https://github.com/Kareadita/Kavita/releases) -[![Docker Pulls](https://img.shields.io/docker/pulls/jvmilazz0/kavita.svg)](https://hub.docker.com/r/jvmilazz0/kavita) +[![Docker Pulls](https://img.shields.io/docker/pulls/kizaing/kavita.svg)](https://hub.docker.com/r/kizaing/kavita/) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Kareadita_Kavita&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=Kareadita_Kavita) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Kareadita_Kavita&metric=security_rating)](https://sonarcloud.io/dashboard?id=Kareadita_Kavita) [![Backers on Open Collective](https://opencollective.com/kavita/backers/badge.svg)](#backers) @@ -24,24 +24,24 @@ your reading collection with your friends and family! ## What Kavita Provides - Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar/rar5, 7zip, raw images) and Books (epub, pdf) - First class responsive readers that work great on any device (phone, tablet, desktop) -- Customizable theming support: [Theme Repo](https://github.com/Kareadita/Themes) and [Documentation](https://wiki.kavitareader.com/guides/themes) -- External metadata integration and scrobbling for read status, ratings, and reviews (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) +- Dark mode and customizable theming support +- External metadata integration and scrobbling for read status, ratings, and reviews (available via Kavita+) - Rich Metadata support with filtering and searching - Ways to group reading material: Collections, Reading Lists (CBL Import), Want to Read - Ability to manage users with rich Role-based management for age restrictions, abilities within the app, etc - Rich web readers supporting webtoon, continuous reading mode (continue without leaving the reader), virtual pages (epub), etc -- Ability to customize your dashboard and side nav with smart filters, custom order and visibility toggles - Full Localization Support -- Ability to download metadata (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) +- Ability to customize your dashboard and side nav with smart filters, custom order and visibility toggles. ## Support +[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/KavitaManga/) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://discord.gg/eczRp9eeem) [![GitHub - Bugs Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Kareadita/Kavita/issues) ## Demo -If you want to try out Kavita, a demo is available: -[https://demo.kavitareader.com/](https://demo.kavitareader.com/login?apiKey=9003cf99-9213-4206-a787-af2fe4cc5f1f) +If you want to try out Kavita, we have a demo up: +[https://demo.kavitareader.com/](https://demo.kavitareader.com/) ``` Username: demouser Password: Demouser64 @@ -50,10 +50,12 @@ Password: Demouser64 ## Setup The easiest way to get started is to visit our Wiki which has up-to-date information on a variety of install methods and platforms. -[https://wiki.kavitareader.com/getting-started](https://wiki.kavitareader.com/getting-started) +[https://wiki.kavitareader.com/en/install](https://wiki.kavitareader.com/en/install) + +**Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.** ## Feature Requests -Got a great idea? Throw it up on [Discussions](https://github.com/Kareadita/Kavita/discussions/2529) or vote on another idea. Many great features in Kavita are driven by our community. +Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features before you submit an idea. ## Notice Kavita is being actively developed and should be considered beta software until the 1.0 release. @@ -62,26 +64,14 @@ vision. You may lose data and have to restart. The Kavita team strives to avoid ## Donate If you like Kavita, have gotten good use out of it, or feel like you want to say thanks with a few bucks, feel free to donate. Money will go towards -expenses related to Kavita. Back us through [OpenCollective](https://opencollective.com/Kavita#backer). You can also use [Paypal](https://www.paypal.com/paypalme/majora2007?locale.x=en_US), however your name will not show below. Kavita+ is also an -option which provides funding, and you get a benefit. +expenses related to Kavita. Back us through [OpenCollective](https://opencollective.com/Kavita#backer). You can also use [Paypal](https://www.paypal.com/paypalme/majora2007?locale.x=en_US), however your name will not show below. ## Kavita+ -[Kavita+](https://wiki.kavitareader.com/kavita+) is a paid subscription that offers premium features that otherwise wouldn't be feasible to include in Kavita. It is ran and operated by majora2007, the creator and developer of Kavita. +[Kavita+](https://wiki.kavitareader.com/en/kavita-plus) is a paid subscription that offers premium features that otherwise wouldn't be feasible to include in Kavita. It is ran and operated by majora2007, the creator and developer of Kavita. If you are interested, you can use the promo code `FIRSTTIME` for your initial signup for a 50% discount on the first month (2$). This can be thought of as donating to Kavita's development and getting some sweet features out of it. -**If you already contribute via OpenCollective, please reach out to majora2007 for a provisioned license.** - -## Localization -Thank you to [Weblate](https://hosted.weblate.org/engage/kavita/) who hosts our localization infrastructure pro bono. If you want to see Kavita in your language, please help us localize. - - -Translation status - - -## PikaPods -If you are looking to try your hand at self-hosting but lack the machine, [PikaPods](https://www.pikapods.com/pods?run=kavita) is a great service that -allows you to easily spin up a server. 20% of app revenues are contributed back to Kavita via OpenCollective. +**If you already contribute via OpenCollective, please reach out to me for a provisioned license.** ## Contributors @@ -107,10 +97,25 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## Mega Sponsors -## Powered By -[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) +## JetBrains +Thank you to [ JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. + +* [ Rider](http://www.jetbrains.com/rider/) +* [ dotTrace](http://www.jetbrains.com/dottrace/) + +## Localization +Thank you to [Weblate](https://hosted.weblate.org/engage/kavita/) who hosts our localization infrastructure pro-bono. If you want to see Kavita in your language, please help us localize. + + + Translation status + + +## PikaPods +If you are looking to try your hand at self-hosting but lack the machine, [PikaPods](https://www.pikapods.com/pods?run=kavita) is a great service that +allows you to easily spin up a server. 20% of app revenues are contributed back to Kavita via OpenCollective. ### License -* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -* Copyright 2020-2024 + +* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) +* Copyright 2020-2023 diff --git a/TestData b/TestData new file mode 160000 index 000000000..4f5750025 --- /dev/null +++ b/TestData @@ -0,0 +1 @@ +Subproject commit 4f5750025a1c0b48cd72eaa6f1b61642c41f147f diff --git a/UI/Web/.editorconfig b/UI/Web/.editorconfig index 28045b9af..2c6908b84 100644 --- a/UI/Web/.editorconfig +++ b/UI/Web/.editorconfig @@ -8,12 +8,6 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -[*.json] -indent_size = 2 - -[en.json] -indent_size = 4 - [*.html] indent_size = 2 diff --git a/UI/Web/.gitignore b/UI/Web/.gitignore index 8132126c9..dbd64df83 100644 --- a/UI/Web/.gitignore +++ b/UI/Web/.gitignore @@ -1,5 +1,3 @@ node_modules/ test-results/ playwright-report/ -i18n-cache-busting.json -e2e-tests/environments/environment.local.ts diff --git a/UI/Web/README.md b/UI/Web/README.md index 4efc47cbc..f088c87cf 100644 --- a/UI/Web/README.md +++ b/UI/Web/README.md @@ -4,7 +4,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular ## Development server -Run `npm run start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Your backend must be served on port 5000. ## Code scaffolding @@ -25,15 +25,7 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github. Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests. -## Connecting to your dev server via your phone or any other compatible client on local network +## Connecting to your dev server via your phone -Update `IP` constant in `src/environments/environment.ts` to your dev machine's ip instead of `localhost`. - -Run `npm run start` - -## Notes: -- injected services should be at the top of the file -- all components must be standalone - -# Update latest angular -`ng update @angular/core @angular/cli @typescript-eslint/parser @angular/localize @angular/compiler-cli @angular-devkit/build-angular @angular/cdk` +ng serve --host 0.0.0.0 +and update environment.ts to your local ip. diff --git a/UI/Web/angular.json b/UI/Web/angular.json index 1ce56fa2e..30a197ec3 100644 --- a/UI/Web/angular.json +++ b/UI/Web/angular.json @@ -24,14 +24,13 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular/build:application", + "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", - "browser": "src/main.ts", + "main": "src/main.ts", "polyfills": [ - "@angular/localize/init", - "zone.js" + "zone.js" ], "inlineStyleLanguage": "scss", "tsConfig": "tsconfig.app.json", @@ -48,20 +47,21 @@ "src/styles.scss", "node_modules/@fortawesome/fontawesome-free/css/all.min.css" ], - "scripts": [], + "scripts": [ + "node_modules/lazysizes/lazysizes.min.js", + "node_modules/lazysizes/plugins/rias/ls.rias.min.js", + "node_modules/lazysizes/plugins/attrchange/ls.attrchange.min.js" + ], "sourceMap": { "hidden": false, "scripts": true, "styles": true }, + "vendorChunk": true, "extractLicenses": false, + "buildOptimizer": false, "optimization": false, - "namedChunks": true, - "stylePreprocessorOptions": { - "sass": { - "silenceDeprecations": ["mixed-decls", "color-functions", "global-builtin", "import"] - } - } + "namedChunks": true }, "configurations": { "production": { @@ -74,8 +74,8 @@ "optimization": true, "outputHashing": "all", "namedChunks": false, - "aot": true, "extractLicenses": true, + "buildOptimizer": true, "budgets": [ { "type": "initial", @@ -84,8 +84,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "30kb" + "maximumWarning": "2kb", + "maximumError": "6kb" } ] } @@ -93,23 +93,23 @@ "defaultConfiguration": "" }, "serve": { - "builder": "@angular/build:dev-server", + "builder": "@angular-devkit/build-angular:dev-server", "options": { "sslKey": "./ssl/server.key", "sslCert": "./ssl/server.crt", "ssl": false, - "buildTarget": "kavita-webui:build" + "browserTarget": "kavita-webui:build" }, "configurations": { "production": { - "buildTarget": "kavita-webui:build:production" + "browserTarget": "kavita-webui:build:production" } } }, "extract-i18n": { - "builder": "@angular/build:extract-i18n", + "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "buildTarget": "kavita-webui:build" + "browserTarget": "kavita-webui:build" } }, "lint": { diff --git a/UI/Web/hash-localization-prime.js b/UI/Web/hash-localization-prime.js deleted file mode 100644 index 013d62b56..000000000 --- a/UI/Web/hash-localization-prime.js +++ /dev/null @@ -1,3 +0,0 @@ -const fs = require('fs'); - -fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify({})); diff --git a/UI/Web/hash-localization.js b/UI/Web/hash-localization.js deleted file mode 100644 index 542ae5127..000000000 --- a/UI/Web/hash-localization.js +++ /dev/null @@ -1,44 +0,0 @@ -const crypto = require('crypto'); -const fs = require('fs'); -const glob = require('glob'); - -const jsonFilesDir = 'dist/browser/assets/langs/'; // Adjust the path to your JSON files -const outputDir = 'dist/browser/assets/langs'; // Directory to store minified files - -function generateChecksum(str, algorithm, encoding) { - return crypto - .createHash(algorithm || 'md5') - .update(str, 'utf8') - .digest(encoding || 'hex'); -} - -const result = {}; - -// Generate directory if it doesn't exist -const distFolderPath = './dist/'; -const browserFolderPath = './dist/browser/'; -if (!fs.existsSync(browserFolderPath)) { - console.log('Creating ./dist/browser folder'); - fs.mkdirSync(distFolderPath, 0o744); - fs.mkdirSync(browserFolderPath, 0o744); -} - -// Remove file if it exists -const cacheBustingFilePath = './i18n-cache-busting.json'; -if (fs.existsSync(cacheBustingFilePath)) { - console.log('Removing existing file') - fs.unlinkSync(cacheBustingFilePath); -} - -glob.sync(`${jsonFilesDir}**/*.json`).forEach(path => { - let tokens = path.split('dist\\browser\\assets\\langs\\'); - if (tokens.length === 1) { - tokens = path.split('dist/browser/assets/langs/'); - } - const lang = tokens[1]; - const content = fs.readFileSync(path, { encoding: 'utf-8' }); - result[lang.replace('.json', '')] = generateChecksum(content); -}); - -fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify(result)); -fs.writeFileSync(`dist/browser/i18n-cache-busting.json`, JSON.stringify(result)); diff --git a/UI/Web/minify-json.js b/UI/Web/minify-json.js index f31e143d1..7799ba2b9 100644 --- a/UI/Web/minify-json.js +++ b/UI/Web/minify-json.js @@ -1,8 +1,8 @@ const fs = require('fs'); const jsonminify = require('jsonminify'); -const jsonFilesDir = 'dist/browser/assets/langs'; // Adjust the path to your JSON files -const outputDir = 'dist/browser/assets/langs'; // Directory to store minified files +const jsonFilesDir = 'dist/assets/langs'; // Adjust the path to your JSON files +const outputDir = 'dist/assets/langs'; // Directory to store minified files fs.readdirSync(jsonFilesDir).forEach(file => { if (file.endsWith('.json')) { diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index cfce8cded..03e677260 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -1,78 +1,78 @@ { "name": "kavita-webui", - "version": "0.7.12.1", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kavita-webui", - "version": "0.7.12.1", + "version": "0.4.2", "dependencies": { - "@angular-slider/ngx-slider": "^19.0.0", - "@angular/animations": "^19.2.5", - "@angular/cdk": "^19.2.8", - "@angular/common": "^19.2.5", - "@angular/compiler": "^19.2.5", - "@angular/core": "^19.2.5", - "@angular/forms": "^19.2.5", - "@angular/localize": "^19.2.5", - "@angular/platform-browser": "^19.2.5", - "@angular/platform-browser-dynamic": "^19.2.5", - "@angular/router": "^19.2.5", - "@fortawesome/fontawesome-free": "^6.7.2", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@iplab/ngx-file-upload": "^19.0.3", - "@jsverse/transloco": "^7.6.1", - "@jsverse/transloco-locale": "^7.0.1", - "@jsverse/transloco-persist-lang": "^7.0.2", - "@jsverse/transloco-persist-translations": "^7.0.1", - "@jsverse/transloco-preload-langs": "^7.0.1", - "@microsoft/signalr": "^8.0.7", - "@ng-bootstrap/ng-bootstrap": "^18.0.0", + "@angular/animations": "^16.2.9", + "@angular/cdk": "^16.2.8", + "@angular/common": "^16.2.9", + "@angular/compiler": "^16.2.9", + "@angular/core": "^16.2.9", + "@angular/forms": "^16.2.9", + "@angular/localize": "^16.2.9", + "@angular/platform-browser": "^16.2.9", + "@angular/platform-browser-dynamic": "^16.2.9", + "@angular/router": "^16.2.9", + "@fortawesome/fontawesome-free": "^6.4.2", + "@iharbeck/ngx-virtual-scroller": "^16.0.0", + "@iplab/ngx-file-upload": "^16.0.2", + "@lithiumjs/angular": "^7.3.0", + "@lithiumjs/ngx-virtual-scroll": "^0.3.0", + "@microsoft/signalr": "^7.0.12", + "@ng-bootstrap/ng-bootstrap": "^15.1.1", + "@ngneat/transloco": "^6.0.0", + "@ngneat/transloco-locale": "^5.1.1", + "@ngneat/transloco-persist-lang": "^5.0.0", + "@ngneat/transloco-persist-translations": "^5.0.0", + "@ngneat/transloco-preload-langs": "^5.0.0", "@popperjs/core": "^2.11.7", - "@siemens/ngx-datatable": "^22.4.1", - "@swimlane/ngx-charts": "^22.0.0-alpha.0", - "@tweenjs/tween.js": "^25.0.0", - "bootstrap": "^5.3.2", - "charts.css": "^1.1.0", + "@swimlane/ngx-charts": "^20.1.2", + "@tweenjs/tween.js": "^21.0.0", + "@types/file-saver": "^2.0.5", + "bootstrap": "^5.3.1", + "eventsource": "^2.0.2", "file-saver": "^2.0.5", - "luxon": "^3.6.1", + "lazysizes": "^5.3.2", + "luxon": "^3.4.3", "ng-circle-progress": "^1.7.1", - "ng-lazyload-image": "^9.1.3", - "ng-select2-component": "^17.2.4", - "ngx-color-picker": "^19.0.0", - "ngx-extended-pdf-viewer": "^23.0.0-alpha.7", + "ng-select2-component": "^13.0.9", + "ngx-color-picker": "^15.0.0", + "ngx-extended-pdf-viewer": "^18.0.2", "ngx-file-drop": "^16.0.0", + "ngx-slider-v2": "^16.0.2", "ngx-stars": "^1.6.5", - "ngx-toastr": "^19.0.0", - "nosleep.js": "^0.12.0", - "rxjs": "^7.8.2", + "ngx-toastr": "^17.0.2", + "rxjs": "^7.8.0", "screenfull": "^6.0.2", "swiper": "^8.4.6", - "tslib": "^2.8.1", - "zone.js": "^0.15.0" + "tslib": "^2.6.2", + "zone.js": "^0.13.0" }, "devDependencies": { - "@angular-eslint/builder": "^19.3.0", - "@angular-eslint/eslint-plugin": "^19.3.0", - "@angular-eslint/eslint-plugin-template": "^19.3.0", - "@angular-eslint/schematics": "^19.3.0", - "@angular-eslint/template-parser": "^19.3.0", - "@angular/build": "^19.2.6", - "@angular/cli": "^19.2.6", - "@angular/compiler-cli": "^19.2.5", - "@types/d3": "^7.4.3", - "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.6.2", - "@types/node": "^22.13.13", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", - "eslint": "^9.23.0", + "@angular-devkit/build-angular": "^16.2.6", + "@angular-eslint/builder": "^16.2.0", + "@angular-eslint/eslint-plugin": "^16.2.0", + "@angular-eslint/eslint-plugin-template": "^16.2.0", + "@angular-eslint/schematics": "^16.2.0", + "@angular-eslint/template-parser": "^16.2.0", + "@angular/cli": "^16.2.6", + "@angular/compiler-cli": "^16.2.9", + "@types/d3": "^7.4.1", + "@types/luxon": "^3.3.2", + "@types/node": "^20.8.6", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "eslint": "^8.51.0", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", - "typescript": "^5.5.4", - "webpack-bundle-analyzer": "^4.10.2" + "typescript": "^5.1.6", + "webpack-bundle-analyzer": "^4.9.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -85,293 +85,123 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.6.tgz", - "integrity": "sha512-Dx6yPxpaE5AhP6UtrVRDCc9Ihq9B65LAbmIh3dNOyeehratuaQS0TYNKjbpaevevJojW840DTg80N+CrlfYp9g==", + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.6.tgz", + "integrity": "sha512-b1NNV3yNg6Rt86ms20bJIroWUI8ihaEwv5k+EoijEXLoMs4eNs5PhqL+QE8rTj+q9pa1gSrWf2blXor2JGwf1g==", "dev": true, "dependencies": { - "@angular-devkit/core": "19.2.6", + "@angular-devkit/core": "16.2.6", "rxjs": "7.8.1" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/@angular-devkit/build-angular": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.6.tgz", + "integrity": "sha512-QdU/q77K1P8CPEEZGxw1QqLcnA9ofboDWS7vcLRBmFmk2zydtLTApbK0P8GNDRbnmROOKkoaLo+xUTDJz9gvPA==", "dev": true, "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/core": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz", - "integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==", - "dev": true, - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^4.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/schematics": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz", - "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "19.2.6", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/build-webpack": "0.1602.6", + "@angular-devkit/core": "16.2.6", + "@babel/core": "7.22.9", + "@babel/generator": "7.22.9", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.22.5", + "@babel/plugin-transform-runtime": "7.22.9", + "@babel/preset-env": "7.22.9", + "@babel/runtime": "7.22.6", + "@babel/template": "7.22.5", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.2.6", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.20", + "css-loader": "6.8.1", + "esbuild-wasm": "0.18.17", + "fast-glob": "3.3.1", + "guess-parser": "0.4.22", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.1", + "mini-css-extract-plugin": "2.7.6", + "mrmime": "1.0.1", + "open": "8.4.2", "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-eslint/builder": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.3.0.tgz", - "integrity": "sha512-j9xNrzZJq29ONSG6EaeQHve0Squkm6u6Dm8fZgWP7crTFOrtLXn7Wxgxuyl9eddpbWY1Ov1gjFuwBVnxIdyAqg==", - "dev": true, - "dependencies": { - "@angular-devkit/architect": ">= 0.1900.0 < 0.2000.0", - "@angular-devkit/core": ">= 19.0.0 < 20.0.0" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.3.0.tgz", - "integrity": "sha512-63Zci4pvnUR1iSkikFlNbShF1tO5HOarYd8fvNfmOZwFfZ/1T3j3bCy9YbE+aM5SYrWqPaPP/OcwZ3wJ8WNvqA==", - "dev": true - }, - "node_modules/@angular-eslint/eslint-plugin": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.3.0.tgz", - "integrity": "sha512-nBLslLI20KnVbqlfNW7GcnI9R6cYCvRGjOE2QYhzxM316ciAQ62tvQuXP9ZVnRBLSKDAVnMeC0eTq9O4ysrxrQ==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.3.0", - "@angular-eslint/utils": "19.3.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.3.0.tgz", - "integrity": "sha512-WyouppTpOYut+wvv13wlqqZ8EHoDrCZxNfGKuEUYK1BPmQlTB8EIZfQH4iR1rFVS28Rw+XRIiXo1x3oC0SOfnA==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.3.0", - "@angular-eslint/utils": "19.3.0", - "aria-query": "5.3.2", - "axobject-query": "4.1.0" - }, - "peerDependencies": { - "@typescript-eslint/types": "^7.11.0 || ^8.0.0", - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/schematics": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.3.0.tgz", - "integrity": "sha512-Wl5sFQ4t84LUb8mJ2iVfhYFhtF55IugXu7rRhPHtgIu9Ty5s1v3HGUx4LKv51m2kWhPPeFOTmjeBv1APzFlmnQ==", - "dev": true, - "dependencies": { - "@angular-devkit/core": ">= 19.0.0 < 20.0.0", - "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", - "@angular-eslint/eslint-plugin": "19.3.0", - "@angular-eslint/eslint-plugin-template": "19.3.0", - "ignore": "7.0.3", - "semver": "7.7.1", - "strip-json-comments": "3.1.1" - } - }, - "node_modules/@angular-eslint/schematics/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@angular-eslint/template-parser": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.3.0.tgz", - "integrity": "sha512-VxMNgsHXMWbbmZeBuBX5i8pzsSSEaoACVpaE+j8Muk60Am4Mxc0PytJm4n3znBSvI3B7Kq2+vStSRYPkOER4lA==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.3.0", - "eslint-scope": "^8.0.2" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/utils": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.3.0.tgz", - "integrity": "sha512-ovvbQh96FIJfepHqLCMdKFkPXr3EbcvYc9kMj9hZyIxs/9/VxwPH7x25mMs4VsL6rXVgH2FgG5kR38UZlcTNNw==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.3.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-slider/ngx-slider": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-slider/ngx-slider/-/ngx-slider-19.0.0.tgz", - "integrity": "sha512-VVJ+Fij5SKnbltxh6TdoBAUAKWfCnSLRPZ7e+r2uO88t8qte5/KHqVOdK4DWCjBr3rEr4YrPR4ylqBCuAWPsKQ==", - "dependencies": { - "detect-passive-events": "^2.0.3", - "rxjs": "^7.8.1", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0", - "@angular/forms": "^19.0.0" - } - }, - "node_modules/@angular/animations": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.5.tgz", - "integrity": "sha512-m4RtY3z1JuHFCh6OrOHxo25oKEigBDdR/XmdCfXIwfTiObZzNA7VQhysgdrb9IISO99kXbjZUYKDtLzgWT8Klg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.5", - "@angular/core": "19.2.5" - } - }, - "node_modules/@angular/build": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.6.tgz", - "integrity": "sha512-+VBLb4ZPLswwJmgfsTFzGex+Sq/WveNc+uaIWyHYjwnuI17NXe1qAAg1rlp72CqGn0cirisfOyAUwPc/xZAgTg==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.6", - "@babel/core": "7.26.10", - "@babel/helper-annotate-as-pure": "7.25.9", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.26.0", - "@inquirer/confirm": "5.1.6", - "@vitejs/plugin-basic-ssl": "1.2.0", - "beasties": "0.2.0", - "browserslist": "^4.23.0", - "esbuild": "0.25.1", - "fast-glob": "3.3.3", - "https-proxy-agent": "7.0.6", - "istanbul-lib-instrument": "6.0.3", - "listr2": "8.2.5", - "magic-string": "0.30.17", - "mrmime": "2.0.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.8.0", - "rollup": "4.34.8", - "sass": "1.85.0", - "semver": "7.7.1", + "picomatch": "2.3.1", + "piscina": "4.0.0", + "postcss": "8.4.31", + "postcss-loader": "7.3.3", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.64.1", + "sass-loader": "13.3.2", + "semver": "7.5.4", + "source-map-loader": "4.0.1", "source-map-support": "0.5.21", - "vite": "6.2.4", - "watchpack": "2.4.2" + "terser": "5.19.2", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.6.1", + "vite": "4.4.7", + "webpack": "5.88.2", + "webpack-dev-middleware": "6.1.1", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.9.0", + "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.2.6" + "esbuild": "0.18.17" }, "peerDependencies": { - "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", - "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", - "@angular/localize": "^19.0.0 || ^19.2.0-next.0", - "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", - "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.6", - "karma": "^6.4.0", - "less": "^4.2.0", - "ng-packagr": "^19.0.0 || ^19.2.0-next.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "typescript": ">=5.5 <5.9" + "@angular/compiler-cli": "^16.0.0", + "@angular/localize": "^16.0.0", + "@angular/platform-server": "^16.0.0", + "@angular/service-worker": "^16.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^16.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.9.3 <5.2" }, "peerDependenciesMeta": { "@angular/localize": { @@ -383,19 +213,19 @@ "@angular/service-worker": { "optional": true }, - "@angular/ssr": { + "jest": { + "optional": true + }, + "jest-environment-jsdom": { "optional": true }, "karma": { "optional": true }, - "less": { - "optional": true - }, "ng-packagr": { "optional": true }, - "postcss": { + "protractor": { "optional": true }, "tailwindcss": { @@ -403,26 +233,26 @@ } } }, - "node_modules/@angular/build/node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "convert-source-map": "^2.0.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", + "json5": "^2.2.2", "semver": "^6.3.1" }, "engines": { @@ -433,7 +263,7 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@angular/build/node_modules/@babel/core/node_modules/semver": { + "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", @@ -442,63 +272,649 @@ "semver": "bin/semver.js" } }, - "node_modules/@angular/build/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", + "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@angular/build/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", + "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", + "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", + "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", + "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", + "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", + "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", + "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", + "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", + "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", + "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", + "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", + "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", + "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", + "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", + "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", + "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", + "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", + "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", + "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", + "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", + "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz", + "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.17", + "@esbuild/android-arm64": "0.18.17", + "@esbuild/android-x64": "0.18.17", + "@esbuild/darwin-arm64": "0.18.17", + "@esbuild/darwin-x64": "0.18.17", + "@esbuild/freebsd-arm64": "0.18.17", + "@esbuild/freebsd-x64": "0.18.17", + "@esbuild/linux-arm": "0.18.17", + "@esbuild/linux-arm64": "0.18.17", + "@esbuild/linux-ia32": "0.18.17", + "@esbuild/linux-loong64": "0.18.17", + "@esbuild/linux-mips64el": "0.18.17", + "@esbuild/linux-ppc64": "0.18.17", + "@esbuild/linux-riscv64": "0.18.17", + "@esbuild/linux-s390x": "0.18.17", + "@esbuild/linux-x64": "0.18.17", + "@esbuild/netbsd-x64": "0.18.17", + "@esbuild/openbsd-x64": "0.18.17", + "@esbuild/sunos-x64": "0.18.17", + "@esbuild/win32-arm64": "0.18.17", + "@esbuild/win32-ia32": "0.18.17", + "@esbuild/win32-x64": "0.18.17" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, - "node_modules/@angular/cdk": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.8.tgz", - "integrity": "sha512-ZZqWVYFF80TdjWkk2sc9Pn2luhiYeC78VH3Yjeln4wXMsTGDsvKPBcuOxSxxpJ31saaVBehDjBUuXMqGRj8KuA==", + "node_modules/@angular-devkit/build-angular/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { - "parse5": "^7.1.2", - "tslib": "^2.3.0" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.6.tgz", + "integrity": "sha512-BJPR6xdq7gRJ6bVWnZ81xHyH75j7lyLbegCXbvUNaM8TWVBkwWsSdqr2NQ717dNLLn5umg58SFpU/pWMq6CxMQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1602.6", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/common": "^19.0.0 || ^20.0.0", - "@angular/core": "^19.0.0 || ^20.0.0", + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.6.tgz", + "integrity": "sha512-iez/8NYXQT6fqVQLlKmZUIRkFUEZ88ACKbTwD4lBmk0+hXW+bQBxI7JOnE3C4zkcM2YeuTXIYsC5SebTKYiR4Q==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.6.tgz", + "integrity": "sha512-PhpRYHCJ3WvZXmng6Qk8TXeQf83jeBMAf7AIzI8h0fgeBocOl97Xf7bZpLg6GymiU+rVn15igQ4Rz9rKAay8bQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.2.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.1", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.2.0.tgz", + "integrity": "sha512-SZjXOi3YIjuX2CocuRsR2QH6k1ca9lRO6IMm0YIYMmBPFCRP2KFHkL6aQnXM6DSaymQNN2TXfpuvUd45NxhU1w==", + "dev": true, + "dependencies": { + "@nx/devkit": "16.5.1", + "nx": "16.5.1" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz", + "integrity": "sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.2.0.tgz", + "integrity": "sha512-zdiAIox1T+B71HL+A8m+1jWdU34nvPGLhCRw/uZNwHzknsF4tYzNQ9W7T/SC/g/2s1yT2yNosEVNJSGSFvunJg==", + "dev": true, + "dependencies": { + "@angular-eslint/utils": "16.2.0", + "@typescript-eslint/utils": "5.62.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.2.0.tgz", + "integrity": "sha512-YFdQ6hHX6NlQj0lfogZwfyKjU8pqkJU+Zsk0ehjlXP8VfKFVmDeQT5/Xr6Df9C8pveC3hvq6Jgd8vo67S9Enxg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.2.0", + "@angular-eslint/utils": "16.2.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "aria-query": "5.3.0", + "axobject-query": "3.2.1" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.2.0.tgz", + "integrity": "sha512-2JUVR7hAKx37mgWeDjvyWEMH5uSeeksYuaQT5wwlgIzgrO4BNFuqs6Rgyp2jiYa7BFMX/qHULSa+bSq5J5ceEA==", + "dev": true, + "dependencies": { + "@angular-eslint/eslint-plugin": "16.2.0", + "@angular-eslint/eslint-plugin-template": "16.2.0", + "@nx/devkit": "16.5.1", + "ignore": "5.2.4", + "nx": "16.5.1", + "strip-json-comments": "3.1.1", + "tmp": "0.2.1" + }, + "peerDependencies": { + "@angular/cli": ">= 16.0.0 < 17.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.2.0.tgz", + "integrity": "sha512-v2jVKTy2wN7iM9nHpBkxLn2wfL8jSl4IlPrXThIqj8No2VHtpLQZPKuXbGPUXQX05VS2Mj5feScQ36ZVGS8Rbw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.2.0", + "eslint-scope": "^7.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.2.0.tgz", + "integrity": "sha512-NxMRwnlIgzmbJQfWkfd9y3Sz0hzjFdK5LH44i+3D5NhpPdZ6SzwHAjMYWoYsmmNQX5tlDXoicYF9Mz9Wz8DJ/A==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.2.0", + "@typescript-eslint/utils": "5.62.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.9.tgz", + "integrity": "sha512-J+nsc2x/ZQuh+YwwTzxXUrV+7SBpJq6DDStfTFkZls9PWGRj9fjqQeRCWrfNLllpxopAEjhFkoyK06oSjcwqAw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.2.9" + } + }, + "node_modules/@angular/cdk": { + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.8.tgz", + "integrity": "sha512-DvqxH909mgSSxWbc5xM5xKLjDMPXY3pzzSVAllngvc9KGPFw240WCs3tSpPaVJI50Esbzdu5O0CyTBfu9jUy4g==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^16.0.0 || ^17.0.0", + "@angular/core": "^16.0.0 || ^17.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.6.tgz", - "integrity": "sha512-eZhFOSsDUHKaciwcWdU5C54ViAvPPdZJf42So93G2vZWDtEq6Uk47huocn1FY9cMhDvURfYLNrrLMpUDtUSsSA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.6.tgz", + "integrity": "sha512-9poPvUEmlufOAW1Cjk+aA5e2x3mInLtbYYSL/EYviDN2ugmavsSIvxAE/WLnxq6cPWqhNDbHDaqvcmqkcFM3Cw==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1902.6", - "@angular-devkit/core": "19.2.6", - "@angular-devkit/schematics": "19.2.6", - "@inquirer/prompts": "7.3.2", - "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.2.6", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", + "@schematics/angular": "16.2.6", "@yarnpkg/lockfile": "1.1.0", - "ini": "5.0.0", - "jsonc-parser": "3.3.1", - "listr2": "8.2.5", - "npm-package-arg": "12.0.2", - "npm-pick-manifest": "10.0.0", - "pacote": "20.0.0", - "resolve": "1.22.10", - "semver": "7.7.1", + "ansi-colors": "4.1.3", + "ini": "4.1.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "15.2.0", + "resolve": "1.22.2", + "semver": "7.5.4", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -506,48 +922,89 @@ "ng": "bin/ng.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@angular/common": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.5.tgz", - "integrity": "sha512-vFCBdas4C5PxP6ts/4TlRddWD3DUmI3aaO0QZdZvqyLHy428t84ruYdsJXKaeD8ie2U4/9F3a1tsklclRG/BBA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.9.tgz", + "integrity": "sha512-5Lh5KsxCkaoBDeSAghKNF5lCi0083ug4X2X7wnafsSd6Z3xt/rDjH9hDOP5SF5IDLtCVjJgHfs3cCLSTjRuNwg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "19.2.5", + "@angular/core": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.5.tgz", - "integrity": "sha512-34J+HubQjwkbZ0AUtU5sa4Zouws9XtP/fKaysMQecoYJTZ3jewzLSRu3aAEZX1Y4gIrcVVKKIxM6oWoXKwYMOA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.9.tgz", + "integrity": "sha512-lh799pnbdvzTVShJHOY1JC6c1pwBsZC4UIgB3Itklo9dskGybQma/gP+lE6RhqM4FblNfaaBXGlCMUuY8HkmEQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.2.9" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } } }, "node_modules/@angular/compiler-cli": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.5.tgz", - "integrity": "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz", + "integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==", "dev": true, "dependencies": { - "@babel/core": "7.26.9", + "@babel/core": "7.22.5", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^4.0.0", + "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.1.2", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^17.2.1" @@ -558,81 +1015,52 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "19.2.5", - "typescript": ">=5.5 <5.9" - } - }, - "node_modules/@angular/compiler-cli/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@angular/compiler-cli/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "dev": true, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "@angular/compiler": "16.2.9", + "typescript": ">=4.9.3 <5.2" } }, "node_modules/@angular/core": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.5.tgz", - "integrity": "sha512-NNEz1sEZz1mBpgf6Tz3aJ9b8KjqpTiMYhHfCYA9h9Ipe4D8gUmOsvPHPK2M755OX7p7PmUmzp1XCUHYrZMVHRw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.9.tgz", + "integrity": "sha512-chvPX29ZBcMDuh7rLIgb0Cru6oJ/0FaqRzfOI3wT4W2F9W1HOlCtipovzmPYaUAmXBWfVP4EBO9TOWnpog0S0w==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" + "zone.js": "~0.13.0" } }, "node_modules/@angular/forms": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.5.tgz", - "integrity": "sha512-2Zvy3qK1kOxiAX9fdSaeG48q7oyO/4RlMYlg1w+ra9qX1SrgwF3OQ2P2Vs+ojg1AxN3z9xFp4aYaaID/G2LZAw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.9.tgz", + "integrity": "sha512-rxlg2iNJNBH/uc7b5YqybfYc8BkLzzPv1d/nMsQUlY0O2UV2zwNRpcIiWbWd7+ZaKjcyPynVe9FsXC8wgWIABw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "19.2.5", - "@angular/core": "19.2.5", - "@angular/platform-browser": "19.2.5", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.5.tgz", - "integrity": "sha512-oAc19bubk6Z/2Vv6OkV0MsjdgC8cUaUwBmwdc6blFVe1NCX1KjdaqDyC2EQAO3nWfcdV4uvOOuu8myxB64bamw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.9.tgz", + "integrity": "sha512-t5002NgBj+wjd81IXwg+yc2ypaBk6OWLAka1GXmWua3x7hwGw1yMtPFmzOE1cCNdXgWlluLxWclFjCUrAbGEww==", "dependencies": { - "@babel/core": "7.26.9", - "@types/babel__core": "7.20.5", - "fast-glob": "3.3.3", + "@babel/core": "7.22.5", + "fast-glob": "3.3.0", "yargs": "^17.2.1" }, "bin": { @@ -641,27 +1069,42 @@ "localize-translate": "tools/bundles/src/translate/cli.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "19.2.5", - "@angular/compiler-cli": "19.2.5" + "@angular/compiler": "16.2.9", + "@angular/compiler-cli": "16.2.9" + } + }, + "node_modules/@angular/localize/node_modules/fast-glob": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" } }, "node_modules/@angular/platform-browser": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.5.tgz", - "integrity": "sha512-Lshy++X16cvl6OPvfzMySpsqEaCPKEJmDjz7q7oSt96oxlh6LvOeOUVLjsNyrNaIt9NadpWoqjlu/I9RTPJkpw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.9.tgz", + "integrity": "sha512-9Je7+Jmx0AOyRzBBumraVJG3M0R6YbT4c9jTUbLGJCcPxwDI3/u2ZzvW3rBqpmrDaqLxN5f1LcZeTZx287QeqQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "19.2.5", - "@angular/common": "19.2.5", - "@angular/core": "19.2.5" + "@angular/animations": "16.2.9", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9" }, "peerDependenciesMeta": { "@angular/animations": { @@ -670,80 +1113,85 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.5.tgz", - "integrity": "sha512-15in8u4552EcdWNTXY2h0MKuJbk3AuXwWr0zVTum4CfB/Ss2tNTrDEdWhgAbhnUI0e9jZQee/fhBbA1rleMYrA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.9.tgz", + "integrity": "sha512-ztpo0939vTZ/5CWVSvo41Yl6YPoTZ0If+yTrs7dk1ce0vFgaZXMlc+y5ZwjJIiMM5CvHbhL48Uk+HJNIojP98A==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "19.2.5", - "@angular/compiler": "19.2.5", - "@angular/core": "19.2.5", - "@angular/platform-browser": "19.2.5" + "@angular/common": "16.2.9", + "@angular/compiler": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9" } }, "node_modules/@angular/router": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.5.tgz", - "integrity": "sha512-9pSfmdNXLjaOKj0kd4UxBC7sFdCFOnRGbftp397G3KWqsLsGSKmNFzqhXNeA5QHkaVxnpmpm8HzXU+zYV5JwSg==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.9.tgz", + "integrity": "sha512-5vrJNMblTDx3WC3dtaqLddWNtR0P9iwpqffeZL1uobBIwP4hbJx+8Dos3TwxGR4hnopFKahoDQ5nC0NOQslyog==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "19.2.5", - "@angular/core": "19.2.5", - "@angular/platform-browser": "19.2.5", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", - "convert-source-map": "^2.0.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "json5": "^2.2.2", + "semver": "^6.3.0" }, "engines": { "node": ">=6.9.0" @@ -753,11 +1201,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -767,40 +1210,51 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -816,26 +1270,21 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -844,70 +1293,313 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", - "dependencies": { - "@babel/types": "^7.26.10" - }, + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -915,13 +1607,48 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -930,43 +1657,1263 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", + "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", + "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", + "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", + "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", + "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", + "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.9.tgz", + "integrity": "sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", + "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.7", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" @@ -1003,26 +2950,10 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", - "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/android-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", - "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -1032,13 +2963,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", - "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -1048,13 +2979,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", - "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -1064,13 +2995,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", - "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -1080,13 +3011,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", - "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -1096,13 +3027,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", - "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -1112,13 +3043,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", - "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -1128,13 +3059,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", - "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -1144,13 +3075,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", - "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -1160,13 +3091,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", - "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -1176,13 +3107,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", - "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -1192,13 +3123,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", - "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -1208,13 +3139,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", - "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -1224,13 +3155,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", - "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -1240,13 +3171,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", - "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -1256,13 +3187,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", - "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -1272,29 +3203,13 @@ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", - "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", - "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ "x64" ], @@ -1304,29 +3219,13 @@ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", - "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", - "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -1336,13 +3235,13 @@ "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", - "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "cpu": [ "x64" ], @@ -1352,13 +3251,13 @@ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", - "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], @@ -1368,13 +3267,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", - "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "cpu": [ "ia32" ], @@ -1384,13 +3283,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", - "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -1400,7 +3299,7 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1419,81 +3318,24 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1501,7 +3343,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1523,118 +3365,87 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.12.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", - "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "hasInstallScript": true, "engines": { "node": ">=6" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=10.10.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1650,346 +3461,35 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true }, "node_modules/@iharbeck/ngx-virtual-scroller": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-19.0.1.tgz", - "integrity": "sha512-dtn4CpbEY92H9nd1A48WNhsyUgtFBjC83xcsc9VzlSQT/KN2fEx0oBs0Obnn6ZdPanDP/IQdlBgmANmlds/wHA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-16.0.0.tgz", + "integrity": "sha512-zdXC/MTamAI5fFxFuFwP/0i+I5mgXulCHflIFITH4MuipLLu1aUkt5o9vbD9QMxehqeRhhQ5nWQVRg6Adosrjg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@tweenjs/tween.js": "^25.0.0" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", - "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", - "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.7", - "@inquirer/type": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", - "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", - "dev": true, - "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", - "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5", - "external-editor": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", - "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", - "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", - "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", - "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5", - "ansi-escapes": "^4.3.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", - "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", - "dev": true, - "dependencies": { - "@inquirer/checkbox": "^4.1.2", - "@inquirer/confirm": "^5.1.6", - "@inquirer/editor": "^4.2.7", - "@inquirer/expand": "^4.0.9", - "@inquirer/input": "^4.1.6", - "@inquirer/number": "^3.0.9", - "@inquirer/password": "^4.0.9", - "@inquirer/rawlist": "^4.0.9", - "@inquirer/search": "^3.0.9", - "@inquirer/select": "^4.0.9" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", - "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", - "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", - "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", - "dev": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@tweenjs/tween.js": "^20.0.3" } }, "node_modules/@iplab/ngx-file-upload": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-19.0.3.tgz", - "integrity": "sha512-PXQroFbMrQwg69b/j6Im9R8DkLz15YxiA0ATlFpOTPRtDhAWQMIRNdxbcqRLmBLdPvrsXpH/gN30f0GyC1k/fw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-16.0.2.tgz", + "integrity": "sha512-6UppO6lROAbkGs+rFZ6ngutsCPrjs/BMnXsIE9UL4AL1NLRRAXrb28pkU2U7KjtDg/0naJ6JFmpRyUBCw5SXhg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^19.0.0", - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0", - "@angular/forms": "^19.0.0", + "@angular/animations": "^16.0.0", + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0", "rxjs": "^7.0.0" } }, @@ -2082,16 +3582,20 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "dependencies": { - "minipass": "^7.0.4" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, "node_modules/@istanbuljs/schema": { @@ -2104,54 +3608,131 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" } }, - "node_modules/@jsverse/transloco": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-7.6.1.tgz", - "integrity": "sha512-hFFKJ1pVFYeW2E4UFETQpOeOn0tuncCSUdRewbq3LiV+qS9x4Z2XVuCaAaFPdiNhy4nUKHWFX1pWjpZ5XjUPaQ==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dependencies": { - "@jsverse/transloco-utils": "^7.0.0", + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@lithiumjs/angular": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@lithiumjs/angular/-/angular-7.3.0.tgz", + "integrity": "sha512-81nXyT9I2J+VpeFEDtOvfP4imlrLueoqFYBZR8PCrlY9cjDzgFAZBq7mCOLxOhi0xL5wF9hM0iDqlmI9LDct1Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=11.0.0 <17.0.0", + "rxjs": ">=7.x.x", + "typescript": ">=4.1.0" + } + }, + "node_modules/@lithiumjs/ngx-virtual-scroll": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@lithiumjs/ngx-virtual-scroll/-/ngx-virtual-scroll-0.3.0.tgz", + "integrity": "sha512-fYZR1S66c4ATg6mDVwJaZxsZ8rT/jcJ07b95x5sZVV7gtiDv7DDUCiMa4mtvj71fqMzcoeRe99G0FivVUwvZ0Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "8.x.x - 16.x.x", + "@angular/core": "8.x.x - 16.x.x", + "@lithiumjs/angular": ">=7.0.0", + "rxjs": "6.x.x - 7.x.x" + } + }, + "node_modules/@microsoft/signalr": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.12.tgz", + "integrity": "sha512-k1Xu+a73PsWgHwHDm6ctHwHTBnlqCzq7L33cbxdWhj90AGDFpxDSzaGCkZDoJFNHveUetix65zIWiazMvmMg3w==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-15.1.1.tgz", + "integrity": "sha512-nZlIMMggtI3IHkGs0XPrUIUdpeEzQvfGV9M4I9IvCqiS2n4RwWoUvWK1ICo4csZqFNBDlCQx956gO6ZZUSL2mw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0", + "@angular/localize": "^16.0.0", + "@popperjs/core": "^2.11.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngneat/transloco": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-6.0.0.tgz", + "integrity": "sha512-hpL3tgY2EYZTTDsCRSN6v+7I0neA0lSLmMVEwTU90ZkRKOV1wIh9TIFXd1slWKklxe2xQHa1AFIn3NkHRP7R+A==", + "dependencies": { + "@ngneat/transloco-utils": "^5.0.0", + "flat": "6.0.0", "fs-extra": "^11.0.0", "glob": "^10.0.0", "lodash.kebabcase": "^4.1.1", @@ -2163,59 +3744,59 @@ "@angular/core": ">=16.0.0" } }, - "node_modules/@jsverse/transloco-locale": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-locale/-/transloco-locale-7.0.1.tgz", - "integrity": "sha512-mx43h2FKMKxx+Er18qArBJMxmGGW2+EShkH+xueAp+VC/ivBNQDyXWpg8hOsfNFqFQAjzlCAie1mXpbGmbM0uw==", + "node_modules/@ngneat/transloco-locale": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-locale/-/transloco-locale-5.1.1.tgz", + "integrity": "sha512-klGQPwYi50hnLkVl619ywttLPigR+zVR4JeeETKyeIJ5bNSNI1oXABPME+CP1Viht2hOsfKdNIQ3GPCIdIJHRQ==", "dependencies": { "tslib": "^2.2.0" }, "peerDependencies": { - "@angular/core": ">=16.0.0", - "@jsverse/transloco": ">=7.0.0", + "@angular/core": ">=13.0.0", + "@ngneat/transloco": ">=4.0.0", "rxjs": ">=6.0.0" } }, - "node_modules/@jsverse/transloco-persist-lang": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-lang/-/transloco-persist-lang-7.0.2.tgz", - "integrity": "sha512-VPB/IbukOS64RUM0NQk2rS/wmezo8JYucYerC/94nyF50LM5tR59SyJTPHSFHBTBqXykOQSXUxLRRwzt8UrfPg==", + "node_modules/@ngneat/transloco-persist-lang": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-persist-lang/-/transloco-persist-lang-5.0.0.tgz", + "integrity": "sha512-vBpHQqTeKZT+V+uvIIEv+KyCq+8HFkCa7lnjvWwcgGupSYjTvZp4PxUm+KOLLmaTIzJDL1OQEaszQ84EzX6Mzg==", "dependencies": { "tslib": "^2.2.0" }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@jsverse/transloco": ">=7.0.0" + "@ngneat/transloco": ">=5.0.0" } }, - "node_modules/@jsverse/transloco-persist-translations": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-translations/-/transloco-persist-translations-7.0.1.tgz", - "integrity": "sha512-BUGpcD4MrIBUbo7/G06yGdkWuVTKXVESyAJp107yUbE34Ami0+4BEK7vfLTl09ARwhBQsNKIzZgTAIpzrlK98A==", + "node_modules/@ngneat/transloco-persist-translations": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-persist-translations/-/transloco-persist-translations-5.0.0.tgz", + "integrity": "sha512-QLM9X9aDRPLZhNK8f8h/4eqjhSJvHoGHRSQ+CoS3qkOXteEdOQXeYzWPHSmvDHc5lN3zNRy6sjHrBQEiZQLCKw==", "dependencies": { "tslib": "^2.2.0" }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@jsverse/transloco": ">=7.0.0" + "@ngneat/transloco": ">=5.0.0" } }, - "node_modules/@jsverse/transloco-preload-langs": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-preload-langs/-/transloco-preload-langs-7.0.1.tgz", - "integrity": "sha512-J9G+r9g8UnLWsEdf0XTUhSIX/CFoKEPP6bEfyXQ7f36FFVu3raPRoEXnqE8gQGCPiyFPG0J8YSf7lyJtUHIgHA==", + "node_modules/@ngneat/transloco-preload-langs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-preload-langs/-/transloco-preload-langs-5.0.0.tgz", + "integrity": "sha512-4ub18hzu5cHcgd7QGUQFl/MqbX8ZsHE6LInY7QFSLG6PpqrKSon+JqXjyWo8hxka8Ae5tMU5fDEEZCRXN19dlw==", "dependencies": { "tslib": "^2.2.0" }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@jsverse/transloco": ">=7.0.0" + "@ngneat/transloco": ">=5.0.0" } }, - "node_modules/@jsverse/transloco-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-7.0.2.tgz", - "integrity": "sha512-zud1M68mMC/Pu6irEba+Z2SzmwmmPzUPnBzMKlcGdIhzUe1z41cqQutK1M0QaQpY4h4yhumXcNaY/Ot6piv6QQ==", + "node_modules/@ngneat/transloco-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-utils/-/transloco-utils-5.0.0.tgz", + "integrity": "sha512-e0S+GWyBTmLix9KfYWW/rScYdqQz3z3znNSb+foaA5T3jWs4CPLVo+PV0No7kGjqom8Wy8H3lLvztfhHxYSLyA==", "dependencies": { "cosmiconfig": "^8.1.3", "tslib": "^2.3.0" @@ -2224,512 +3805,31 @@ "node": ">=16" } }, - "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", - "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", - "dev": true, - "dependencies": { - "@inquirer/type": "^1.5.5" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8" - } - }, - "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", - "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", - "dev": true, - "dependencies": { - "mute-stream": "^1.0.0" + "node_modules/@ngneat/transloco/node_modules/flat": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.0.tgz", + "integrity": "sha512-/aYW7n5yD9HyW9ZHCzW2LGSpyTmbXEdRWdH5ded6K/a2ETgowOripjBqJorQ4/PBa3VPFub28fNruWp+onmIUg==", + "bin": { + "flat": "cli.js" }, "engines": { "node": ">=18" } }, - "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "node_modules/@ngtools/webpack": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.6.tgz", + "integrity": "sha512-d8ZlZL6dOtWmHdjG9PTGBkdiJMcsXD2tp6WeFRVvTEuvCI3XvKsUXBvJDE+mZOhzn5pUEYt+1TR5DHjDZbME3w==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", - "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", - "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", - "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", - "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", - "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", - "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@microsoft/signalr": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz", - "integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==", - "dependencies": { - "abort-controller": "^3.0.0", - "eventsource": "^2.0.2", - "fetch-cookie": "^2.0.3", - "node-fetch": "^2.6.7", - "ws": "^7.4.5" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@napi-rs/nice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", - "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/nice-android-arm-eabi": "1.0.1", - "@napi-rs/nice-android-arm64": "1.0.1", - "@napi-rs/nice-darwin-arm64": "1.0.1", - "@napi-rs/nice-darwin-x64": "1.0.1", - "@napi-rs/nice-freebsd-x64": "1.0.1", - "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", - "@napi-rs/nice-linux-arm64-gnu": "1.0.1", - "@napi-rs/nice-linux-arm64-musl": "1.0.1", - "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", - "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", - "@napi-rs/nice-linux-s390x-gnu": "1.0.1", - "@napi-rs/nice-linux-x64-gnu": "1.0.1", - "@napi-rs/nice-linux-x64-musl": "1.0.1", - "@napi-rs/nice-win32-arm64-msvc": "1.0.1", - "@napi-rs/nice-win32-ia32-msvc": "1.0.1", - "@napi-rs/nice-win32-x64-msvc": "1.0.1" - } - }, - "node_modules/@napi-rs/nice-android-arm-eabi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", - "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-android-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", - "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", - "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-freebsd-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", - "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", - "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", - "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", - "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-ppc64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", - "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-riscv64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", - "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-s390x-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", - "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", - "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", - "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-arm64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", - "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-ia32-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", - "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-x64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", - "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@ng-bootstrap/ng-bootstrap": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-18.0.0.tgz", - "integrity": "sha512-GeSAz4yiGq49psdte8kcf+Y562wB3jK/qKRAkh6iA32lcXmy2sfQXVAmlHdjZ3AyP+E8lf3yMwuPdSKiYcDgSg==", - "dependencies": { - "tslib": "^2.3.0" + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0", - "@angular/forms": "^19.0.0", - "@angular/localize": "^19.0.0", - "@popperjs/core": "^2.11.8", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/compiler-cli": "^16.0.0", + "typescript": ">=4.9.3 <5.2", + "webpack": "^5.54.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -2764,278 +3864,186 @@ "node": ">= 8" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", "dev": true, "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/git": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", - "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", "dev": true, "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^5.0.0" + "which": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", "dev": true, "dependencies": { - "isexe": "^3.1.1" + "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", "dev": true, "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" }, "bin": { - "installed-package-contents": "bin/index.js" + "installed-package-contents": "lib/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", - "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/package-json": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", - "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", - "dev": true, - "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", - "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", "dev": true, "dependencies": { - "which": "^5.0.0" + "which": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", "dev": true, "dependencies": { - "isexe": "^3.1.1" + "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/redact": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.1.1.tgz", - "integrity": "sha512-3Hc2KGIkrvJWJqTbvueXzBeZlmvoOxc2jyX00yzr3+sNFquJg0N8hH4SAPLPVrkWIRQICVpVgjrss971awXVnA==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/run-script": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", - "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", "dev": true, "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", "dev": true, "dependencies": { - "isexe": "^3.1.1" + "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "node_modules/@nrwl/devkit": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.5.1.tgz", + "integrity": "sha512-NB+DE/+AFJ7lKH/WBFyatJEhcZGj25F24ncDkwjZ6MzEiSOGOJS0LaV/R+VUsmS5EHTPXYOpn3zHWWAcJhyOmA==", "dev": true, - "hasInstallScript": true, - "optional": true, "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@nx/devkit": "16.5.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], + "node_modules/@nrwl/tao": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-16.5.1.tgz", + "integrity": "sha512-x+gi/fKdM6uQNIti9exFlm3V5LBP3Y8vOEziO42HdOigyrXa0S0HD2WMpccmp6PclYKhwEDUjKJ39xh5sdh4Ig==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "nx": "16.5.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "tao": "index.js" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "node_modules/@nx/devkit": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-16.5.1.tgz", + "integrity": "sha512-T1acZrVVmJw/sJ4PIGidCBYBiBqlg/jT9e8nIGXLSDS20xcLvfo4zBQf8UZLrmHglnwwpDpOWuVJCp2rYA5aDg==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "16.5.1", + "ejs": "^3.1.7", + "ignore": "^5.0.4", + "semver": "7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "nx": ">= 15 <= 17" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.5.1.tgz", + "integrity": "sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q==", "cpu": [ "arm64" ], @@ -3045,17 +4053,13 @@ "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "node_modules/@nx/nx-darwin-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz", + "integrity": "sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==", "cpu": [ "x64" ], @@ -3065,17 +4069,13 @@ "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "node_modules/@nx/nx-freebsd-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz", + "integrity": "sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==", "cpu": [ "x64" ], @@ -3085,17 +4085,13 @@ "freebsd" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz", + "integrity": "sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==", "cpu": [ "arm" ], @@ -3105,37 +4101,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz", + "integrity": "sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==", "cpu": [ "arm64" ], @@ -3145,17 +4117,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz", + "integrity": "sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==", "cpu": [ "arm64" ], @@ -3165,17 +4133,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", + "integrity": "sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==", "cpu": [ "x64" ], @@ -3185,17 +4149,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "node_modules/@nx/nx-linux-x64-musl": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz", + "integrity": "sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==", "cpu": [ "x64" ], @@ -3205,17 +4165,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz", + "integrity": "sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==", "cpu": [ "arm64" ], @@ -3225,37 +4181,13 @@ "win32" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz", + "integrity": "sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==", "cpu": [ "x64" ], @@ -3264,6 +4196,20 @@ "os": [ "win32" ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", + "integrity": "sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^3.2.1", + "node-gyp-build": "^4.3.0" + }, "engines": { "node": ">= 10.0.0" }, @@ -3272,26 +4218,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "optional": true - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3302,9 +4228,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", "dev": true }, "node_modules/@popperjs/core": { @@ -3316,393 +4242,88 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@schematics/angular": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.6.tgz", - "integrity": "sha512-fmbF9ONmEZqxHocCwOSWG2mHp4a22d1uW+DZUBUgZSBUFIrnFw42deOxDq8mkZOZ1Tc73UpLN2GKI7iJeUqS2A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.6.tgz", + "integrity": "sha512-fM09WPqST+nhVGV5Q3fhG7WKo96kgSVMsbz3wGS0DmTn4zge7ZWnrW3VvbxnMapmGoKa9DFPqdqNln4ADcdIMQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "19.2.6", - "@angular-devkit/schematics": "19.2.6", - "jsonc-parser": "3.3.1" + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", + "jsonc-parser": "3.2.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@siemens/ngx-datatable": { - "version": "22.4.1", - "resolved": "https://registry.npmjs.org/@siemens/ngx-datatable/-/ngx-datatable-22.4.1.tgz", - "integrity": "sha512-Z19zaxu7tpwMHWc1h5Om9/sZJ39MWTQypju6T6WH7QIkelKgZE7DbYk3siD41vkR/62vT+q0Z1voC2OyxgRX9g==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": ">=17.0.0", - "@angular/core": ">=17.0.0", - "@angular/platform-browser": ">=17.0.0", - "rxjs": "^7.8.0" - } - }, - "node_modules/@sigstore/bundle": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", - "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", - "dev": true, - "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@sigstore/core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", - "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.0.tgz", - "integrity": "sha512-o09cLSIq9EKyRXwryWDOJagkml9XgQCoCSRjHOnHLnvsivaW7Qznzz6yjfV7PHJHhIvyp8OH7OX8w0Dc5bQK7A==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz", + "integrity": "sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@sigstore/sign": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", - "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@sigstore/tuf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.0.tgz", - "integrity": "sha512-suVMQEA+sKdOz5hwP9qNcEjX6B45R+hFFr4LAWzbRc5O+U2IInwvay/bpG5a4s+qR35P/JK/PiKiRGjfuLy1IA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.2.tgz", + "integrity": "sha512-vjwcYePJzM01Ha6oWWZ9gNcdIgnzyFxfqfWzph483DPJTH8Tb7f7bQRRll3CYVkyH56j0AgcPAcl6Vg95DPF+Q==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0", - "tuf-js": "^3.0.1" + "@sigstore/protobuf-specs": "^0.1.0", + "tuf-js": "^1.1.7" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@sigstore/verify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.0.tgz", - "integrity": "sha512-kAAM06ca4CzhvjIZdONAL9+MLppW3K48wOFy1TbuaWFW/OMfl8JuTgW0Bm02JB1WJGT/ET2eqav0KTEKmxqkIA==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@swimlane/ngx-charts": { - "version": "22.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-22.0.0-alpha.0.tgz", - "integrity": "sha512-sauI4QcfpuKXmRWajpeVtAoT7z8uI3u1+hvfcsJ796LRr06C676dkjoZsk7aX3EU+6uF8mJpXClOT/JcfnZrEA==", + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.4.1.tgz", + "integrity": "sha512-DyTQe0fcqLDoLEZca45gkdjxP8iLH7kh4pCkr+TCFIkmgEdfQ5DpavNBOOVO0qd5J5uV/tbtSnkYWSx8JkbFpg==", "dependencies": { - "d3-array": "^3.2.0", + "d3-array": "^3.1.1", "d3-brush": "^3.0.0", "d3-color": "^3.1.0", "d3-ease": "^3.0.1", "d3-format": "^3.1.0", - "d3-hierarchy": "^3.1.2", + "d3-hierarchy": "^3.1.0", "d3-interpolate": "^3.0.1", - "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.2.0", - "d3-time-format": "^4.1.0", + "d3-time-format": "^3.0.0", "d3-transition": "^3.0.1", - "gradient-path": "^2.3.0", - "tslib": "^2.3.1" + "rfdc": "^1.3.0", + "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/animations": "17.x || 18.x || 19.x", - "@angular/cdk": "17.x || 18.x || 19.x", - "@angular/common": "17.x || 18.x || 19.x", - "@angular/core": "17.x || 18.x || 19.x", - "@angular/forms": "17.x || 18.x || 19.x", - "@angular/platform-browser": "17.x || 18.x || 19.x", - "@angular/platform-browser-dynamic": "17.x || 18.x || 19.x", - "rxjs": "7.x" + "@angular/animations": ">=12.0.0", + "@angular/cdk": ">=12.0.0", + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@angular/platform-browser-dynamic": ">=12.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" } }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", "dev": true }, "node_modules/@tsconfig/node12": { @@ -3724,73 +4345,98 @@ "dev": true }, "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", + "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", "dev": true, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@tufjs/models": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", - "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", "dev": true, "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tweenjs/tween.js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==" + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz", + "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@types/body-parser": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", + "dev": true, "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "node_modules/@types/bonjour": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.11.tgz", + "integrity": "sha512-isGhjmBtLIxdHBDl2xGwUzEM8AOyOvWsADWq7rqirdi/ZQoHnLWErHvsThcEzTX8juDRiZtzp2Qkv5bgNh6mAg==", + "dev": true, "dependencies": { - "@babel/types": "^7.0.0" + "@types/node": "*" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "dev": true, "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@types/node": "*" } }, - "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", + "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", + "dev": true, "dependencies": { - "@babel/types": "^7.20.7" + "@types/express-serve-static-core": "*", + "@types/node": "*" } }, "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.1.tgz", + "integrity": "sha512-lBpYmbHTCtFKO1DB1R7E9dXp9/g1F3JXSGOF7iKPZ+wRmYg/Q6tCRHODGOc5Qk25fJRe2PI60EDRf2HLPUncMA==", "dev": true, "dependencies": { "@types/d3-array": "*", @@ -3826,45 +4472,45 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.5.tgz", + "integrity": "sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==", "dev": true }, "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.2.tgz", + "integrity": "sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==", "dev": true, "dependencies": { "@types/d3-selection": "*" } }, "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", "dev": true, "dependencies": { "@types/d3-selection": "*" } }, "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.2.tgz", + "integrity": "sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==", "dev": true }, "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", "dev": true }, "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.2.tgz", + "integrity": "sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==", "dev": true, "dependencies": { "@types/d3-array": "*", @@ -3872,282 +4518,508 @@ } }, "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", "dev": true }, "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz", + "integrity": "sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==", "dev": true }, "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.2.tgz", + "integrity": "sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==", "dev": true, "dependencies": { "@types/d3-selection": "*" } }, "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==", "dev": true }, "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", "dev": true }, "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.2.tgz", + "integrity": "sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==", "dev": true, "dependencies": { "@types/d3-dsv": "*" } }, "node_modules/@types/d3-force": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", - "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.4.tgz", + "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==", "dev": true }, "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", "dev": true }, "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.3.tgz", + "integrity": "sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==", "dev": true, "dependencies": { "@types/geojson": "*" } }, "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==", "dev": true }, "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", "dev": true, "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", "dev": true }, "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", "dev": true }, "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", "dev": true }, "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", "dev": true }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", "dev": true, "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", "dev": true }, "node_modules/@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.5.tgz", + "integrity": "sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==", "dev": true }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", + "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", "dev": true, "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", "dev": true }, "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", "dev": true }, "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", "dev": true }, "node_modules/@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", "dev": true, "dependencies": { "@types/d3-selection": "*" } }, "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.3.tgz", + "integrity": "sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==", "dev": true, "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, + "node_modules/@types/eslint": { + "version": "8.40.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", + "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.19.tgz", + "integrity": "sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/file-saver": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", - "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==" }, "node_modules/@types/geojson": { - "version": "7946.0.14", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", "dev": true }, + "node_modules/@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.12", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz", + "integrity": "sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/luxon": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", - "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", + "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", "dev": true }, "node_modules/@types/node": { - "version": "22.13.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", - "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "version": "20.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz", + "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==", "dev": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~5.25.1" } }, - "node_modules/@types/tinycolor2": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==" + "node_modules/@types/qs": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "dev": true }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "node_modules/@types/range-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-asaEIoc6J+DbBKXtO7p2shWUpKacZOoMBEGBgPG91P8xhO53ohzHWGCs4ScZo5pQMf5ukQzVT9fhX1WzpHihig==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.34.tgz", + "integrity": "sha512-R+n7qBFnm/6jinlteC9DBL5dGiDGjWAvjo4viUanpnc/dG1y7uDoacXPIQ/PQEg1fI912SMHIa014ZjRpvDw4g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", + "integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz", + "integrity": "sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.5", + "@typescript-eslint/type-utils": "6.7.5", + "@typescript-eslint/utils": "6.7.5", + "@typescript-eslint/visitor-keys": "6.7.5", + "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^5.2.4", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz", + "integrity": "sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/typescript-estree": "6.7.5", + "@typescript-eslint/utils": "6.7.5", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.5.tgz", + "integrity": "sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.5", + "@typescript-eslint/types": "6.7.5", + "@typescript-eslint/typescript-estree": "6.7.5", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.5.tgz", + "integrity": "sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.7.5", + "@typescript-eslint/types": "6.7.5", + "@typescript-eslint/typescript-estree": "6.7.5", + "@typescript-eslint/visitor-keys": "6.7.5", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz", + "integrity": "sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "6.7.5", + "@typescript-eslint/visitor-keys": "6.7.5" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4155,35 +5027,96 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "tsutils": "^3.21.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.5.tgz", + "integrity": "sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==", "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4191,110 +5124,516 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz", + "integrity": "sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/types": "6.7.5", + "@typescript-eslint/visitor-keys": "6.7.5", "debug": "^4.3.4", - "fast-glob": "^3.3.2", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz", + "integrity": "sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "6.7.5", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", "dev": true, "engines": { - "node": ">=14.21.3" + "node": ">=14.6.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@wessberg/ts-evaluator": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/@wessberg/ts-evaluator/-/ts-evaluator-0.0.27.tgz", + "integrity": "sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA==", + "deprecated": "this package has been renamed to ts-evaluator. Please install ts-evaluator instead", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "jsdom": "^16.4.0", + "object-path": "^0.11.5", + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/wessberg/ts-evaluator?sponsor=1" + }, + "peerDependencies": { + "typescript": ">=3.2.x || >= 4.x" + } + }, + "node_modules/@wessberg/ts-evaluator/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@wessberg/ts-evaluator/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wessberg/ts-evaluator/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@wessberg/ts-evaluator/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@wessberg/ts-evaluator/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wessberg/ts-evaluator/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true }, - "node_modules/abbrev": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz", - "integrity": "sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==", + "node_modules/@yarnpkg/parsers": { + "version": "3.0.0-rc.46", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", + "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=14.15.0" } }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", + "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@zkochan/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4306,10 +5645,23 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4318,6 +5670,46 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4328,33 +5720,90 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, "engines": { "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, "engines": { - "node": ">= 14" + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", + "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" }, "funding": { "type": "github", @@ -4362,9 +5811,9 @@ } }, "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "dependencies": { "ajv": "^8.0.0" @@ -4378,6 +5827,27 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4393,6 +5863,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4402,17 +5884,46 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/arg": { @@ -4422,26 +5933,182 @@ "dev": true }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", + "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.32.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/balanced-match": { @@ -4468,23 +6135,28 @@ } ] }, - "node_modules/beasties": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", - "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "dependencies": { - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^9.1.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" - }, "engines": { - "node": ">=14.0.0" + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/bl": { @@ -4497,6 +6169,66 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4504,9 +6236,9 @@ "dev": true }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", "funding": [ { "type": "github", @@ -4522,28 +6254,36 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dependencies": { - "fill-range": "^7.1.1" + "fill-range": "^7.0.1" }, "engines": { "node": ">=8" } }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -4559,10 +6299,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -4600,83 +6340,67 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", "dev": true, "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "semver": "^7.0.0" } }, - "node_modules/cacache/node_modules/chownr": { + "node_modules/bytes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true, "engines": { - "node": ">=18" + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.3.tgz", + "integrity": "sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/callsites": { @@ -4687,10 +6411,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001543", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", + "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", "funding": [ { "type": "opencollective", @@ -4707,18 +6440,16 @@ ] }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4" } }, "node_modules/chardet": { @@ -4727,10 +6458,32 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/charts.css": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/charts.css/-/charts.css-1.1.0.tgz", - "integrity": "sha512-K1Qyb8ZKsu5cDrVbZeHECk/xSq6iOl8IDTR35uaMdhr/Vyyxvg9nYQy3KNB3aidxJ2E251afX5q2725N0uL3Vw==" + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } }, "node_modules/chownr": { "version": "2.0.0", @@ -4741,6 +6494,24 @@ "node": ">=10" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4763,108 +6534,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true, "engines": { - "node": ">= 12" + "node": ">= 10" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", + "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clone": { @@ -4875,21 +6562,41 @@ "node": ">=0.8" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, "dependencies": { - "color-name": "~1.1.4" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } }, "node_modules/colorette": { "version": "2.0.20", @@ -4897,26 +6604,249 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js-compat": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", + "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", "dependencies": { - "import-fresh": "^3.3.0", + "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", + "parse-json": "^5.0.0", "path-type": "^4.0.0" }, "engines": { @@ -4924,14 +6854,22 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/create-require": { @@ -4940,10 +6878,95 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/critters": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.20.tgz", + "integrity": "sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4953,6 +6976,32 @@ "node": ">= 8" } }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -4981,6 +7030,42 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -5078,41 +7163,6 @@ "node": ">=12" } }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" - }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -5159,16 +7209,34 @@ } }, "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" + "d3-time": "1 - 2" } }, + "node_modules/d3-time-format/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-time-format/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/d3-time-format/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -5195,18 +7263,61 @@ "d3-selection": "2 - 3" } }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "engines": { + "node": ">=10.4" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "^2.1.3" + "ms": "2.1.2" }, "engines": { "node": ">=6.0" @@ -5217,12 +7328,30 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -5234,20 +7363,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-it": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/detect-it/-/detect-it-4.0.1.tgz", "integrity": "sha512-dg5YBTJYvogK1+dA2mBUDKzOWfYZtHVba89SyZUhc4+e3i2tzgjANFg5lDRCd3UOtRcw00vUTMK8LELcMdicug==" }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true }, "node_modules/detect-passive-events": { "version": "2.0.3", @@ -5266,6 +7443,48 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5300,6 +7519,27 @@ } ] }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -5316,9 +7556,9 @@ } }, "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "dependencies": { "dom-serializer": "^2.0.0", @@ -5329,6 +7569,15 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -5340,16 +7589,55 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.46", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.46.tgz", - "integrity": "sha512-1XDk0Z8/YRgB2t5GeEg8DPK592DLjVmd/5uwAu6c/S4Z0CUwV/RwYqe5GWxQqcoN3bJ5U7hYMiMRPZzpCzSBhQ==" + "version": "1.4.540", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz", + "integrity": "sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg==" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -5373,10 +7661,45 @@ "node": ">=0.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, "engines": { "node": ">=0.12" }, @@ -5393,24 +7716,25 @@ "node": ">=6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5419,125 +7743,179 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, "node_modules/esbuild": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", - "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.1", - "@esbuild/android-arm": "0.25.1", - "@esbuild/android-arm64": "0.25.1", - "@esbuild/android-x64": "0.25.1", - "@esbuild/darwin-arm64": "0.25.1", - "@esbuild/darwin-x64": "0.25.1", - "@esbuild/freebsd-arm64": "0.25.1", - "@esbuild/freebsd-x64": "0.25.1", - "@esbuild/linux-arm": "0.25.1", - "@esbuild/linux-arm64": "0.25.1", - "@esbuild/linux-ia32": "0.25.1", - "@esbuild/linux-loong64": "0.25.1", - "@esbuild/linux-mips64el": "0.25.1", - "@esbuild/linux-ppc64": "0.25.1", - "@esbuild/linux-riscv64": "0.25.1", - "@esbuild/linux-s390x": "0.25.1", - "@esbuild/linux-x64": "0.25.1", - "@esbuild/netbsd-arm64": "0.25.1", - "@esbuild/netbsd-x64": "0.25.1", - "@esbuild/openbsd-arm64": "0.25.1", - "@esbuild/openbsd-x64": "0.25.1", - "@esbuild/sunos-x64": "0.25.1", - "@esbuild/win32-arm64": "0.25.1", - "@esbuild/win32-ia32": "0.25.1", - "@esbuild/win32-x64": "0.25.1" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.18.17.tgz", + "integrity": "sha512-9OHGcuRzy+I8ziF9FzjfKLWAPbvi0e/metACVg9k6bK+SI4FFxeV6PcZsz8RIVaMD4YNehw+qj6UMR3+qj/EuQ==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" } }, "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", - "@eslint/plugin-kit": "^0.2.7", - "@humanfs/node": "^0.16.6", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.2", "debug": "^4.3.2", + "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5571,16 +7949,61 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5593,18 +8016,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5633,6 +8044,42 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5654,18 +8101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5696,33 +8131,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "has-flag": "^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=4" } }, "node_modules/esquery": { @@ -5767,6 +8227,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -5775,12 +8244,27 @@ "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", "dev": true }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", @@ -5789,10 +8273,102 @@ "node": ">=12.0.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/exponential-backoff": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", - "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/external-editor": { @@ -5809,6 +8385,18 @@ "node": ">=4" } }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5816,15 +8404,16 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "micromatch": "^4.0.4" }, "engines": { "node": ">=8.6.0" @@ -5842,39 +8431,60 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true - }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fetch-cookie": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", - "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.1.0.tgz", + "integrity": "sha512-39+cZRbWfbibmj22R2Jy6dmTbAWC+oqun1f1FzQaNurkPDUP4C38jpeZbiXCR88RKRVDp8UcDrbFXkNhN+NjYg==", "dependencies": { "set-cookie-parser": "^2.4.8", "tough-cookie": "^4.0.0" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "dependencies": { - "flat-cache": "^4.0.0" + "escape-string-regexp": "^1.0.5" }, "engines": { - "node": ">=16.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/file-saver": { @@ -5882,10 +8492,40 @@ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5893,25 +8533,116 @@ "node": ">=8" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=16" + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -5927,10 +8658,61 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -5941,26 +8723,32 @@ } }, "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", + "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", "dev": true, "dependencies": { - "minipass": "^7.0.3" + "minipass": "^5.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/fs-monkey": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "dev": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "optional": true, @@ -5972,14 +8760,36 @@ } }, "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5996,31 +8806,55 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "engines": { - "node": ">=18" + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", + "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", + "jackspeak": "^2.0.3", "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "minipass": "^5.0.0 || ^6.0.2", + "path-scurry": "^1.10.0" }, "bin": { - "glob": "dist/esm/bin.mjs" + "glob": "dist/cjs/src/bin.js" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6046,6 +8880,28 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -6054,25 +8910,49 @@ "node": ">=4" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/gradient-path": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gradient-path/-/gradient-path-2.3.0.tgz", - "integrity": "sha512-vZdF/Z0EpqUztzWXFjFC16lqcialHacYoRonslk/bC6CuujkuIrqx7etlzdYHY4SnUU94LRWESamZKfkGh7yYQ==", - "dependencies": { - "tinygradient": "^1.0.0" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/guess-parser": { + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/guess-parser/-/guess-parser-0.4.22.tgz", + "integrity": "sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg==", + "dev": true, + "dependencies": { + "@wessberg/ts-evaluator": "0.0.27" + }, + "peerDependencies": { + "typescript": ">=3.7.5" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -6088,44 +8968,170 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "dependencies": { - "function-bind": "^1.1.2" + "function-bind": "^1.1.1" }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hosted-git-info": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", - "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", "dev": true, "dependencies": { - "lru-cache": "^10.0.1" + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6133,9 +9139,9 @@ "dev": true }, "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -6147,8 +9153,8 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.0.1", + "entities": "^4.4.0" } }, "node_modules/http-cache-semantics": { @@ -6157,30 +9163,115 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { - "node": ">= 14" + "node": ">= 0.8" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "dependencies": { - "agent-base": "^7.1.2", + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 14" + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" } }, "node_modules/iconv-lite": { @@ -6195,6 +9286,18 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6215,30 +9318,67 @@ ] }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/ignore-walk": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", - "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", + "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", "dev": true, "dependencies": { "minimatch": "^9.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/immutable": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", - "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", "dev": true }, "node_modules/import-fresh": { @@ -6273,6 +9413,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6288,12 +9437,108 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/internmap": { @@ -6304,17 +9549,19 @@ "node": ">=12" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", "dev": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { - "node": ">= 12" + "node": ">= 10" } }, "node_modules/is-arrayish": { @@ -6322,21 +9569,45 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "dependencies": { - "hasown": "^2.0.2" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6372,6 +9643,12 @@ "node": ">=8" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6380,6 +9657,57 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -6391,15 +9719,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, "engines": { "node": ">=8" @@ -6431,17 +9792,38 @@ } }, "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", + "make-dir": "^3.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/istanbul-lib-source-maps": { @@ -6468,9 +9850,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -6481,9 +9863,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", + "integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6497,53 +9879,298 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { - "argparse": "^2.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "node_modules/jsdom/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "engines": { + "node": ">=10.4" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, "node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -6569,9 +10196,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, "node_modules/jsonfile": { @@ -6621,35 +10248,126 @@ "node": ">=10.0.0" } }, - "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "source-map-support": "^0.5.5" } }, - "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazysizes": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz", + "integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==" + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" }, "engines": { - "node": "*" + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", "dev": true, "dependencies": { - "json-buffer": "3.0.1" + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/levn": { @@ -6665,127 +10383,96 @@ "node": ">= 0.8.0" } }, - "node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" + "webpack-sources": "^3.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } } }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6.11.5" } }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==" }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "node_modules/lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true }, - "node_modules/lmdb": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", - "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "msgpackr": "^1.11.2", - "node-addon-api": "^6.1.0", - "node-gyp-build-optional-packages": "5.2.2", - "ordered-binary": "^1.5.3", - "weak-lru-cache": "^1.2.2" - }, - "bin": { - "download-lmdb-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.2.6", - "@lmdb/lmdb-darwin-x64": "3.2.6", - "@lmdb/lmdb-linux-arm": "3.2.6", - "@lmdb/lmdb-linux-arm64": "3.2.6", - "@lmdb/lmdb-linux-x64": "3.2.6", - "@lmdb/lmdb-win32-x64": "3.2.6" - } + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.invokemap": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz", + "integrity": "sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==", + "dev": true }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -6798,6 +10485,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.pullall": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz", + "integrity": "sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==", + "dev": true + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6813,194 +10512,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { - "restore-cursor": "^5.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "get-east-asian-width": "^1.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=7.0.0" } }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "dependencies": { - "mimic-function": "^5.0.0" - }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { + "node_modules/log-symbols/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/lru-cache": { @@ -7012,37 +10585,49 @@ } }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", "engines": { "node": ">=12" } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" } }, "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "dependencies": { - "semver": "^7.5.3" + "semver": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -7050,27 +10635,73 @@ "dev": true }, "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", "dev": true, "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", + "negotiator": "^0.6.3", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7079,27 +10710,58 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dependencies": { - "braces": "^3.0.3", + "braces": "^3.0.2", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/mimic-fn": { @@ -7110,64 +10772,102 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" } }, "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "dev": true, "dependencies": { - "minipass": "^7.0.3" + "minipass": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 8" } }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" + "yallist": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -7203,6 +10903,34 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -7264,33 +10992,36 @@ "dev": true }, "node_modules/minizlib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", - "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" + "minipass": "^3.0.0", + "yallist": "^4.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 8" } }, - "node_modules/minizlib/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "dependencies": { - "glob": "^10.3.7" + "yallist": "^4.0.0" }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8" } }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7304,64 +11035,42 @@ } }, "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", "dev": true, "engines": { "node": ">=10" } }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/msgpackr": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", - "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "optional": true, - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "dev": true, - "hasInstallScript": true, - "optional": true, "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" }, "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + "multicast-dns": "cli.js" } }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, "funding": [ { @@ -7382,15 +11091,62 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "engines": { "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/ng-circle-progress": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/ng-circle-progress/-/ng-circle-progress-1.7.1.tgz", @@ -7404,37 +11160,24 @@ "rxjs": ">=6.4.0" } }, - "node_modules/ng-lazyload-image": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-9.1.3.tgz", - "integrity": "sha512-GlajmzbKhQCvg9pcrASq4fe/MNv9KoifGe6N+xRbseaBrNj2uwU4Vwic041NlmAQFEkpDM1H2EJCAjjmJeF7Hg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": ">=11.0.0", - "@angular/core": ">=11.0.0", - "rxjs": ">=6.0.0" - } - }, "node_modules/ng-select2-component": { - "version": "17.2.4", - "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.4.tgz", - "integrity": "sha512-pfRQg1gY1NsQkBNAYYeSYJjejKwz1z+9bKWor8/8toCNbvh9TYMOKpcz3FrNvhR6v/Hto/quddajaxjD81TOgg==", + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.9.tgz", + "integrity": "sha512-Bj7lHCiHnwNFECyzpn0LyD3IOPnBbIHHYXxpFU313QZgVkEz7oiF9nBnkorAAABIfLk4EiU0nBQkY3CmbVOgfg==", "dependencies": { - "ngx-infinite-scroll": ">=18.0.0 || >=19.0.0", + "ngx-infinite-scroll": ">=16.0.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": ">=18.1.0 || >=19.0.0", - "@angular/common": ">=18.1.0 || >=19.0.0", - "@angular/core": ">=18.1.0 || >=19.0.0" + "@angular/cdk": ">=16.1.0", + "@angular/common": ">=16.1.0", + "@angular/core": ">=16.1.0" } }, "node_modules/ngx-color-picker": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-19.0.0.tgz", - "integrity": "sha512-jZs7nk/DJB6FryElYnfkojWYCgpEc650s800g+39ebocVMZ18fAHf/CQd5+Bdm4E3zoRod0a0sErJ+c8tGQcCg==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-15.0.0.tgz", + "integrity": "sha512-+7wK8Pz9pm7ywJQOWELRcLYO9J0q4giF4b5QFxq8J3kEcHsUBn0hKOpBbGud+UmNnOwbJVgU2rhyRpGIDUCDJw==", "dependencies": { "tslib": "^2.3.0" }, @@ -7445,15 +11188,16 @@ } }, "node_modules/ngx-extended-pdf-viewer": { - "version": "23.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-23.0.0-alpha.7.tgz", - "integrity": "sha512-S5jI9Z6p6wglLwvpf85MddxGKYUiJczb02nZcFWztDSZ7BlKXkjdtssW+chBOc/sg46p2kTDoa0M/R07yqRFcA==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-18.0.2.tgz", + "integrity": "sha512-CgRR39Fb0ZLsuzvNbiFPXuJL72bAqnvJyhytVQjsOo66ZQMnQA9dkU/YbkDKr25Btk/+PtAJmSkbBpPgyNxj1Q==", "dependencies": { + "lodash.deburr": "^4.1.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=17.0.0 <20.0.0", - "@angular/core": ">=17.0.0 <20.0.0" + "@angular/common": ">=12.0.0 <17.0.0", + "@angular/core": ">=12.0.0 <17.0.0" } }, "node_modules/ngx-file-drop": { @@ -7473,15 +11217,30 @@ } }, "node_modules/ngx-infinite-scroll": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-19.0.0.tgz", - "integrity": "sha512-Ft4xNNDLXoDGi2hF6ylehjxbG8JIgfoL6qDWWcebGMcbh1CEfEsh0HGkDuFlX/cBBMenRh2HFbXlYq8BAtbvLw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-16.0.0.tgz", + "integrity": "sha512-bzyNYd+wVlUUxcopRVr2DAa81eEc8vITtKVvb+c7R1uy8hWPTlxOEXf3L1qA4FMwTEzCQ9b37TXzlJji3qBy+A==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=19.0.0 <20.0.0", - "@angular/core": ">=19.0.0 <20.0.0" + "@angular/common": ">=16.0.0 <17.0.0", + "@angular/core": ">=16.0.0 <17.0.0" + } + }, + "node_modules/ngx-slider-v2": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-16.0.2.tgz", + "integrity": "sha512-Lpl7SlErL+tJJvTRZYdyZoXTThKN8Ro1z3vscJQ1O5azHXwvbv3pnTcsOwY4ltfaP+dpzY27KL1QXyDr6QMaxQ==", + "dependencies": { + "detect-passive-events": "^2.0.3", + "rxjs": "^7.4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0" } }, "node_modules/ngx-stars": { @@ -7497,9 +11256,9 @@ } }, "node_modules/ngx-toastr": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz", - "integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-17.0.2.tgz", + "integrity": "sha512-KehiPx6bkbiUyJbabf0ZA04+ASumS8r/y4wPsUOMI9OrBvBcfq27UQmWuQKoVR8E+9y4Pq7eZkSg2kvxNvpxTA==", "dependencies": { "tslib": "^2.3.0" }, @@ -7509,17 +11268,31 @@ "@angular/platform-browser": ">=16.0.0-0" } }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -7535,236 +11308,242 @@ } } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.1.0.tgz", - "integrity": "sha512-/+7TuHKnBpnMvUQnsYEb0JOozDZqarQbfNuSGLXIjhStMT0fbw7IdSqWgopOP5xhRZE+lsbIvAHcekddruPZgQ==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz", + "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==", "dev": true, "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", + "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "make-fetch-happen": "^11.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", "semver": "^7.3.5", - "tar": "^7.4.3", - "which": "^5.0.0" + "tar": "^6.1.2", + "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, - "optional": true, "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", "dev": true, "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/nosleep.js": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", - "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" - }, - "node_modules/npm-bundled": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", "dev": true, "dependencies": { - "npm-normalize-package-bin": "^4.0.0" + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-install-checks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", - "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", + "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", "dev": true, "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", "dev": true, "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-packlist": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", - "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", "dev": true, "dependencies": { - "ignore-walk": "^7.0.0" + "ignore-walk": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-pick-manifest": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", - "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", "dev": true, "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-registry-fetch": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", - "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", "dev": true, "dependencies": { - "@npmcli/redact": "^3.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/nth-check": { @@ -7779,6 +11558,260 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/nx": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-16.5.1.tgz", + "integrity": "sha512-I3hJRE4hG7JWAtncWwDEO3GVeGPpN0TtM8xH5ArZXyDuVeTth/i3TtJzdDzqXO1HHtIoAQN0xeq4n9cLuMil5g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nrwl/tao": "16.5.1", + "@parcel/watcher": "2.0.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.0-rc.46", + "@zkochan/js-yaml": "0.0.6", + "axios": "^1.0.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^7.0.2", + "dotenv": "~10.0.0", + "enquirer": "~2.3.6", + "fast-glob": "3.2.7", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^11.1.0", + "glob": "7.1.4", + "ignore": "^5.0.4", + "js-yaml": "4.1.0", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "3.0.5", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "semver": "7.5.3", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "v8-compile-cache": "2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "16.5.1", + "@nx/nx-darwin-x64": "16.5.1", + "@nx/nx-freebsd-x64": "16.5.1", + "@nx/nx-linux-arm-gnueabihf": "16.5.1", + "@nx/nx-linux-arm64-gnu": "16.5.1", + "@nx/nx-linux-arm64-musl": "16.5.1", + "@nx/nx-linux-x64-gnu": "16.5.1", + "@nx/nx-linux-x64-musl": "16.5.1", + "@nx/nx-win32-arm64-msvc": "16.5.1", + "@nx/nx-win32-x64-msvc": "16.5.1" + }, + "peerDependencies": { + "@swc-node/register": "^1.4.2", + "@swc/core": "^1.2.173" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nx/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/nx/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/nx/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nx/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/nx/node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nx/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/nx/node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nx/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz", + "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-path": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", + "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==", + "dev": true, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7801,6 +11834,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -7849,12 +11899,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ordered-binary": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", - "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", - "dev": true, - "optional": true + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/os-tmpdir": { "version": "1.0.2", @@ -7865,49 +11972,117 @@ "node": ">=0.10.0" } }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pacote": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", - "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", + "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", "tar": "^6.1.11" }, "bin": { - "pacote": "bin/index.js" + "pacote": "lib/bin.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7936,20 +12111,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, "node_modules/parse-json/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, "dependencies": { "entities": "^4.4.0" }, @@ -7983,6 +12163,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7992,6 +12181,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8007,12 +12205,12 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.0.tgz", + "integrity": "sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8022,13 +12220,19 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", + "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", "engines": { "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8038,35 +12242,146 @@ } }, "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/piscina": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", - "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.0.0.tgz", + "integrity": "sha512-641nAmJS4k4iqpNUqfggqUBUMmlw0ZoM5VZKdQkV2e970Inn3Tk9kroCc1wpsYLD07vCwpys5iY0d3xI/9WkTg==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, "optionalDependencies": { - "@napi-rs/nice": "^1.0.1" + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -8083,18 +12398,112 @@ } ], "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "node_modules/postcss-loader": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", + "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.2.0", + "jiti": "^1.18.2", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, "node_modules/prelude-ls": { @@ -8106,15 +12515,39 @@ "node": ">= 0.8.0" } }, - "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -8128,19 +12561,69 @@ "node": ">=10" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "engines": { "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8165,6 +12648,94 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8178,16 +12749,105 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/replace-in-file": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-7.1.0.tgz", - "integrity": "sha512-1uZmJ78WtqNYCSuPC9IWbweXkGxPOtk2rKuar8diTw7naVIQZiE3Tm8ACx2PCMXDtVH6N+XxwaRY2qZ2xHPqXw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-7.0.1.tgz", + "integrity": "sha512-KbhgPq04eA+TxXuUxpgWIH9k/TjF+28ofon2PXP7vq6izAILhxOtksCVcLuuQLtyjouBaPdlH6RJYYcSPVxCOA==", "dependencies": { "chalk": "^4.1.2", "glob": "^8.1.0", @@ -8200,6 +12860,59 @@ "node": ">=10" } }, + "node_modules/replace-in-file/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/replace-in-file/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/replace-in-file/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/replace-in-file/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -8218,6 +12931,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/replace-in-file/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/replace-in-file/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -8229,6 +12950,17 @@ "node": ">=10" } }, + "node_modules/replace-in-file/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8252,25 +12984,70 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8307,49 +13084,70 @@ } }, "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, - "node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "dependencies": { - "@types/estree": "1.0.6" + "glob": "^7.1.3" }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.26.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz", + "integrity": "sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==", + "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18.0.0", + "node": ">=14.18.0", "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8373,9 +13171,9 @@ } }, "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dependencies": { "tslib": "^2.1.0" } @@ -8406,13 +13204,13 @@ "dev": true }, "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", + "integrity": "sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==", "dev": true, "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -8420,37 +13218,81 @@ }, "engines": { "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/sass-loader": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", + "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", "dev": true, "dependencies": { - "readdirp": "^4.0.1" + "neo-async": "^2.6.2" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 14.15.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } } }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, + "optional": true + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, "engines": { - "node": ">= 14.18.0" + "node": ">=10" + } + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/screenfull": { @@ -8464,11 +13306,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -8476,11 +13339,200 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8500,10 +13552,33 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", "engines": { "node": ">=14" }, @@ -8512,74 +13587,43 @@ } }, "node_modules/sigstore": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", - "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.7.0.tgz", + "integrity": "sha512-KP7QULhWdlu3hlp+jw2EvgWKlOGOY9McLj/jrchLjHNlNPK0KWIwF919cbmOp6QiKXLmPijR2qH/5KYWlbtG9Q==", "dev": true, "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "@sigstore/sign": "^3.1.0", - "@sigstore/tuf": "^3.1.0", - "@sigstore/verify": "^2.1.0" + "@sigstore/protobuf-specs": "^0.1.0", + "@sigstore/tuf": "^1.0.1", + "make-fetch-happen": "^11.0.1" + }, + "bin": { + "sigstore": "bin/sigstore.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", + "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", "dev": true, "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", "totalist": "^3.0.0" }, "engines": { "node": ">= 10" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/smart-buffer": { @@ -8592,32 +13636,43 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, "dependencies": { - "ip-address": "^9.0.5", + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.0.0", + "node": ">= 10.13.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "dev": true, "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" }, "engines": { - "node": ">= 14" + "node": ">= 10" } }, "node_modules/source-map": { @@ -8630,14 +13685,47 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -8668,9 +13756,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -8684,15 +13772,45 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "node_modules/ssr-window": { @@ -8701,15 +13819,24 @@ "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==" }, "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", + "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", "dev": true, "dependencies": { - "minipass": "^7.0.3" + "minipass": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" } }, "node_modules/string_decoder": { @@ -8770,6 +13897,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8782,15 +13927,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + }, + "bin": { + "sl-log-transformer": "bin/sl-log-transformer.js" }, "engines": { - "node": ">=8" + "node": ">=4" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -8837,10 +13999,25 @@ "node": ">=0.10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dev": true, "dependencies": { "chownr": "^2.0.0", @@ -8854,6 +14031,22 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -8878,70 +14071,183 @@ "node": ">=8" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" - }, - "node_modules/tinygradient": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", - "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", - "dependencies": { - "@types/tinycolor2": "^1.4.0", - "tinycolor2": "^1.0.0" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "node_modules/terser": { + "version": "5.19.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", + "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", "dev": true, "dependencies": { - "os-tmpdir": "~1.0.2" + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" }, "engines": { - "node": ">=0.6.0" + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" } }, "node_modules/to-regex-range": { @@ -8955,6 +14261,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -8991,22 +14306,31 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", "dev": true, "engines": { - "node": ">=18.12" + "node": ">=16.13.0" }, "peerDependencies": { - "typescript": ">=4.8.4" + "typescript": ">=4.2.0" } }, "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -9046,23 +14370,58 @@ } } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/tuf-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", - "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tuf-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", + "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", + "dev": true, + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/type-check": { @@ -9089,10 +14448,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -9103,47 +14481,96 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", "dev": true }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "dependencies": { - "unique-slug": "^5.0.0" + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "engines": { "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -9159,8 +14586,8 @@ } ], "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "escalade": "^3.1.1", + "picocolors": "^1.0.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -9192,6 +14619,30 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -9209,56 +14660,61 @@ } }, "node_modules/validate-npm-package-name": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", - "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.8" } }, "node_modules/vite": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", - "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", + "integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==", "dev": true, "dependencies": { - "esbuild": "^0.25.0", - "postcss": "^8.5.3", - "rollup": "^4.30.1" + "esbuild": "^0.18.10", + "postcss": "^8.4.26", + "rollup": "^3.25.2" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.3" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", + "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", - "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -9268,9 +14724,6 @@ "sass": { "optional": true }, - "sass-embedded": { - "optional": true - }, "stylus": { "optional": true }, @@ -9279,19 +14732,35 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dev": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -9301,6 +14770,15 @@ "node": ">=10.13.0" } }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -9309,32 +14787,77 @@ "defaults": "^1.0.3" } }, - "node_modules/weak-lru-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", - "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", - "dev": true, - "optional": true - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz", + "integrity": "sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", - "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "lodash.debounce": "^4.0.8", + "lodash.escape": "^4.0.1", + "lodash.flatten": "^4.4.0", + "lodash.invokemap": "^4.6.0", + "lodash.pullall": "^4.2.0", + "lodash.uniqby": "^4.7.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", @@ -9368,6 +14891,298 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-bundle-analyzer/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", + "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -9391,18 +15206,35 @@ "node": ">= 8" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { @@ -9422,15 +15254,75 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "engines": { "node": ">=8.3.0" }, @@ -9447,6 +15339,18 @@ } } }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9485,6 +15389,19 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -9506,22 +15423,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zone.js": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", - "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==" + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz", + "integrity": "sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==", + "dependencies": { + "tslib": "^2.3.0" + } } } } diff --git a/UI/Web/package.json b/UI/Web/package.json index 05d539aed..ba4128128 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -1,85 +1,82 @@ { "name": "kavita-webui", - "version": "0.7.12.1", + "version": "0.4.2", "scripts": { "ng": "ng", - "start": "npm run cache-locale && ng serve --host 0.0.0.0", - "build": "npm run cache-locale && ng build", + "start": "ng serve", + "build": "ng build", "minify-langs": "node minify-json.js", - "cache-locale": "node hash-localization.js", - "cache-locale-prime": "node hash-localization-prime.js", - "sync-locale": "node sync-locales.js", - "prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale", + "prod": "ng build --configuration production --aot --output-hashing=all && npm run minify-langs", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { - "@angular-slider/ngx-slider": "^19.0.0", - "@angular/animations": "^19.2.5", - "@angular/cdk": "^19.2.8", - "@angular/common": "^19.2.5", - "@angular/compiler": "^19.2.5", - "@angular/core": "^19.2.5", - "@angular/forms": "^19.2.5", - "@angular/localize": "^19.2.5", - "@angular/platform-browser": "^19.2.5", - "@angular/platform-browser-dynamic": "^19.2.5", - "@angular/router": "^19.2.5", - "@fortawesome/fontawesome-free": "^6.7.2", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@iplab/ngx-file-upload": "^19.0.3", - "@jsverse/transloco": "^7.6.1", - "@jsverse/transloco-locale": "^7.0.1", - "@jsverse/transloco-persist-lang": "^7.0.2", - "@jsverse/transloco-persist-translations": "^7.0.1", - "@jsverse/transloco-preload-langs": "^7.0.1", - "@microsoft/signalr": "^8.0.7", - "@ng-bootstrap/ng-bootstrap": "^18.0.0", + "@angular/animations": "^16.2.9", + "@angular/cdk": "^16.2.8", + "@angular/common": "^16.2.9", + "@angular/compiler": "^16.2.9", + "@angular/core": "^16.2.9", + "@angular/forms": "^16.2.9", + "@angular/localize": "^16.2.9", + "@angular/platform-browser": "^16.2.9", + "@angular/platform-browser-dynamic": "^16.2.9", + "@angular/router": "^16.2.9", + "@fortawesome/fontawesome-free": "^6.4.2", + "@iharbeck/ngx-virtual-scroller": "^16.0.0", + "@iplab/ngx-file-upload": "^16.0.2", + "@lithiumjs/angular": "^7.3.0", + "@lithiumjs/ngx-virtual-scroll": "^0.3.0", + "@microsoft/signalr": "^7.0.12", + "@ng-bootstrap/ng-bootstrap": "^15.1.1", + "@ngneat/transloco": "^6.0.0", + "@ngneat/transloco-locale": "^5.1.1", + "@ngneat/transloco-persist-lang": "^5.0.0", + "@ngneat/transloco-persist-translations": "^5.0.0", + "@ngneat/transloco-preload-langs": "^5.0.0", "@popperjs/core": "^2.11.7", - "@siemens/ngx-datatable": "^22.4.1", - "@swimlane/ngx-charts": "^22.0.0-alpha.0", - "@tweenjs/tween.js": "^25.0.0", - "bootstrap": "^5.3.2", - "charts.css": "^1.1.0", + "@swimlane/ngx-charts": "^20.1.2", + "@tweenjs/tween.js": "^21.0.0", + "@types/file-saver": "^2.0.5", + "bootstrap": "^5.3.1", + "eventsource": "^2.0.2", "file-saver": "^2.0.5", - "luxon": "^3.6.1", + "lazysizes": "^5.3.2", + "luxon": "^3.4.3", "ng-circle-progress": "^1.7.1", - "ng-lazyload-image": "^9.1.3", - "ng-select2-component": "^17.2.4", - "ngx-color-picker": "^19.0.0", - "ngx-extended-pdf-viewer": "^23.0.0-alpha.7", + "ng-select2-component": "^13.0.9", + "ngx-color-picker": "^15.0.0", + "ngx-extended-pdf-viewer": "^18.0.2", "ngx-file-drop": "^16.0.0", + "ngx-slider-v2": "^16.0.2", "ngx-stars": "^1.6.5", - "ngx-toastr": "^19.0.0", - "nosleep.js": "^0.12.0", - "rxjs": "^7.8.2", + "ngx-toastr": "^17.0.2", + "rxjs": "^7.8.0", "screenfull": "^6.0.2", "swiper": "^8.4.6", - "tslib": "^2.8.1", - "zone.js": "^0.15.0" + "tslib": "^2.6.2", + "zone.js": "^0.13.0" }, "devDependencies": { - "@angular-eslint/builder": "^19.3.0", - "@angular-eslint/eslint-plugin": "^19.3.0", - "@angular-eslint/eslint-plugin-template": "^19.3.0", - "@angular-eslint/schematics": "^19.3.0", - "@angular-eslint/template-parser": "^19.3.0", - "@angular/build": "^19.2.6", - "@angular/cli": "^19.2.6", - "@angular/compiler-cli": "^19.2.5", - "@types/d3": "^7.4.3", - "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.6.2", - "@types/node": "^22.13.13", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", - "eslint": "^9.23.0", + "@angular-devkit/build-angular": "^16.2.6", + "@angular-eslint/builder": "^16.2.0", + "@angular-eslint/eslint-plugin": "^16.2.0", + "@angular-eslint/eslint-plugin-template": "^16.2.0", + "@angular-eslint/schematics": "^16.2.0", + "@angular-eslint/template-parser": "^16.2.0", + "@angular/cli": "^16.2.6", + "@angular/compiler-cli": "^16.2.9", + "@types/d3": "^7.4.1", + "@types/luxon": "^3.3.2", + "@types/node": "^20.8.6", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "eslint": "^8.51.0", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", - "typescript": "^5.5.4", - "webpack-bundle-analyzer": "^4.10.2" + "typescript": "^5.1.6", + "webpack-bundle-analyzer": "^4.9.1" } } diff --git a/UI/Web/src/_card-item-common.scss b/UI/Web/src/_card-item-common.scss deleted file mode 100644 index 1c6f916f3..000000000 --- a/UI/Web/src/_card-item-common.scss +++ /dev/null @@ -1,246 +0,0 @@ -$image-height: 232.91px; -$image-width: 160px; - -.error-banner { - width: $image-width; - height: 18px; - background-color: var(--toast-error-bg-color); - font-size: 12px; - color: white; - text-transform: uppercase; - text-align: center; - - position: absolute; - top: 0px; - right: 0px; -} - -.selected-highlight { - outline: 2px solid var(--primary-color); -} - -.progress-banner { - width: $image-width; - height: 5px; - - .progress { - color: var(--card-progress-bar-color); - background-color: transparent; - } -} - -.download { - width: 80px; - height: 80px; - position: absolute; - top: 25%; - right: 30%; -} - -.badge-container { - border-radius: 4px; - display: block; - height: $image-height; - left: 0; - overflow: hidden; - pointer-events: none; - position: absolute; - top: 0; - width: 160px; -} - -.not-read-badge { - position: absolute; - top: calc(-1 * (var(--card-progress-triangle-size) / 2)); - right: -14px; - z-index: 1000; - height: var(--card-progress-triangle-size); - width: var(--card-progress-triangle-size); - background-color: var(--primary-color); - transform: rotate(45deg); -} - -.bulk-mode { - position: absolute; - top: 5px; - left: 5px; - visibility: hidden; - - &.always-show { - visibility: visible !important; - width: $image-width; - height: $image-height; - } - - input[type="checkbox"] { - width: 20px; - height: 20px; - color: var(--checkbox-bg-color); - } -} - -.meta-title { - display: none; - visibility: hidden; - pointer-events: none; - border-width: 0; -} - -.overlay { - &:hover { - .bulk-mode { - visibility: visible; - z-index: 110; - } - - &:hover { - visibility: visible; - - .overlay-information { - visibility: visible; - display: block; - } - - & + .meta-title { - display: -webkit-box; - visibility: visible; - pointer-events: none; - } - } - - .overlay-information { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 232.91px; - transition: all 0.2s; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - - &:hover { - background-color: var(--card-overlay-hover-bg-color); - cursor: pointer; - } - - .overlay-information--centered { - position: absolute; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 50px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 115; - - &:hover { - background-color: var(--primary-color) !important; - cursor: pointer; - } - } - } - } - - .count { - top: 5px; - right: 10px; - position: absolute; - } -} - -.card-actions { - z-index: 115; -} - -.library { - font-size: 13px; - text-decoration: none; - margin-top: 0px; -} - -.card-title-container { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 5px; - - :first-child { - min-width: 22px; - } - - .card-title { - font-size: 0.8rem; - margin: 0; - text-align: center; - max-width: 90px; - - a { - overflow: hidden; - text-overflow: ellipsis; - } - } -} - -.card-actions { - min-width: 15.82px; -} - -.card-format { - min-width: 22px; -} - -::ng-deep app-card-actionables .dropdown .dropdown-toggle { - padding: 0 5px; -} - -.meta-title { - .card-title { - max-width: unset; - } -} - -.card-title { - font-size: 0.8rem; - margin: 0; - padding: 10px 0; - text-align: center; - max-width: 120px; - - a { - overflow: hidden; - text-overflow: ellipsis; - } -} - -.card-body > div:nth-child(2) { - height: 40px; - overflow: hidden; - -webkit-line-clamp: 2; - display: -webkit-box; - overflow: hidden; - -webkit-box-orient: vertical; - font-size: 0.8rem; -} - -.overlay-information { - visibility: hidden; - display: none; - .card-title { - padding: 10px; - } -} - -.chapter, -.volume, -.series, -.expected { - .overlay-information--centered { - div { - height: 32px; - width: 32px; - i { - font-size: 1.4rem; - line-height: 32px; - } - } - } -} diff --git a/UI/Web/src/_manga-reader-common.scss b/UI/Web/src/_manga-reader-common.scss index 9b54a5fad..1f1af75fa 100644 --- a/UI/Web/src/_manga-reader-common.scss +++ b/UI/Web/src/_manga-reader-common.scss @@ -1,4 +1,4 @@ -$scrollbarHeight: 35px; +$scrollbarHeight: 34px; img { user-select: none; @@ -9,31 +9,29 @@ img { align-items: center; &.full-width { - height: 100dvh; + height: calc(var(--vh)*100); display: grid; } &.full-height { - height: calc(100dvh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos + height: calc(100vh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos display: flex; align-content: center; - overflow-y: hidden; } &.original { - height: calc(100dvh); + height: 100vh; display: grid; } .full-height { width: auto; margin: auto; - max-height: calc(100dvh); - height: calc(100dvh); + max-height: calc(var(--vh)*100); + overflow: hidden; // This technically will crop and make it just fit vertical-align: top; - object-fit: cover; &.wide { - height: calc(100dvh); + height: 100vh; } } @@ -48,13 +46,12 @@ img { width: 100%; margin: 0 auto; vertical-align: top; - object-fit: contain; - width: 100%; + max-width: fit-content; } .fit-to-screen.full-width { width: 100%; - max-height: calc(100dvh); + max-height: calc(var(--vh)*100); } } diff --git a/UI/Web/src/_series-detail-common.scss b/UI/Web/src/_series-detail-common.scss deleted file mode 100644 index efb54f860..000000000 --- a/UI/Web/src/_series-detail-common.scss +++ /dev/null @@ -1,209 +0,0 @@ -@use './theme/variables' as theme; - -.title { - color: white; - font-weight: bold; - font-size: 1.75rem; -} - -.image-container { - align-self: flex-start; - max-height: 400px; - max-width: 280px; -} - -.subtitle { - color: var(--detail-subtitle-color); - font-weight: bold; - font-size: 0.8rem; -} - -.main-container { - overflow: unset !important; - margin-top: 15px; -} - -::ng-deep .badge-expander .content a { - font-size: 0.8rem; -} - -.btn-group > .btn.dropdown-toggle-split:not(first-child){ - border-top-right-radius: var(--bs-border-radius) !important; - border-bottom-right-radius: var(--bs-border-radius) !important; - border-width: 1px 1px 1px 0 !important; -} - -.btn-group > .btn:not(:last-child):not(.dropdown-toggle) { - border-width: 1px 0 1px 1px !important; -} - -.card-body > div:nth-child(2) { - height: 50px; - overflow: hidden; - -webkit-line-clamp: 2; - display: -webkit-box; - overflow: hidden; - -webkit-box-orient: vertical; -} - -.under-image ~ .overlay-information { - top: -404px; - height: 364px; -} - -.overlay-information { - position: relative; - top: -364px; - height: 364px; - transition: all 0.2s; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - - &:hover { - cursor: pointer; - background-color: var(--card-overlay-hover-bg-color) !important; - - .overlay-information--centered { - visibility: visible; - } - } - - .overlay-information--centered { - position: absolute; - border-radius: 15px; - background-color: rgba(0, 0, 0, .7); - border-radius: 50px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 115; - visibility: hidden; - - &:hover { - background-color: var(--primary-color) !important; - cursor: pointer; - } - - div { - width: 60px; - height: 60px; - i { - font-size: 1.6rem; - line-height: 60px; - width: 100%; - } - } - } -} - -.progress { - border-radius: 0; -} - -.progress-banner.series { - position: relative; -} - -::ng-deep .progress-banner.series span { - position: absolute; - left: 50%; - transform: translate(-50%, -50%); - color: white; - top: 50%; -} - -.carousel-tabs-container { - overflow-x: auto; - white-space: nowrap; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - scrollbar-width: none; - box-shadow: inset -1px -2px 0px -1px var(--elevation-layer9); -} -.carousel-tabs-container::-webkit-scrollbar { - display: none; -} -.nav-tabs { - flex-wrap: nowrap; -} - -.upper-details { - font-size: 0.9rem; -} - -::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen{ - border-width: 1px; - border-style: solid; - border-radius: 5px; - border-color: var(--primary-color); - padding: 5px; - vertical-align: middle; - - &:hover { - background-color: var(--primary-color-dark-shade); - } -} - -::ng-deep .image-container.mobile-bg app-image img { - max-height: 400px; - object-fit: contain; -} - -@media (max-width: theme.$grid-breakpoints-lg) { - .carousel-tabs-container { - mask-image: linear-gradient(transparent, black 0%, black 90%, transparent 100%); - -webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); - } -} - -::ng-deep .image-container.mobile-bg app-image img { - max-height: 100dvh !important; - object-fit: cover !important; -} - -/* col-lg */ -@media (max-width: theme.$grid-breakpoints-lg) { - .image-container.mobile-bg{ - width: 100vw; - top: calc(var(--nav-offset) - 20px); - left: 0; - pointer-events: none; - position: fixed !important; - display: block !important; - max-height: unset !important; - max-width: unset !important; - height: 100dvh !important; - } - - ::ng-deep .image-container.mobile-bg app-image img { - max-height: unset !important; - opacity: 0.05 !important; - filter: blur(5px) !important; - max-width: 100dvw; - height: 100dvh !important; - overflow: hidden; - position: absolute; - top: 0; - left: 0; - object-fit: cover; - } - - .progress-banner { - display:none; - } - - .under-image { - display: none; - } - -} -.upper-details { - font-size: 0.9rem; -} - -@media (max-width: theme.$grid-breakpoints-lg) { - .carousel-tabs-container { - mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); - -webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%); - } -} diff --git a/UI/Web/src/_tag-card-common.scss b/UI/Web/src/_tag-card-common.scss deleted file mode 100644 index 39a1e87fd..000000000 --- a/UI/Web/src/_tag-card-common.scss +++ /dev/null @@ -1,35 +0,0 @@ -.tag-card { - background-color: var(--bs-card-color, #2c2c2c); - padding: 1rem; - border-radius: 12px; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - transition: transform 0.2s ease, background 0.3s ease; - cursor: pointer; - - &.not-selectable:hover { - cursor: not-allowed; - background-color: var(--bs-card-color, #2c2c2c) !important; - } -} - -.tag-card:hover { - background-color: #3a3a3a; - //transform: translateY(-3px); // Cool effect but has a weird background issue. ROBBIE: Fix this -} - -.tag-name { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 0.5rem; - max-height: 8rem; - height: 8rem; - overflow: hidden; - text-overflow: ellipsis; -} - -.tag-meta { - font-size: 0.85rem; - display: flex; - justify-content: space-between; - color: var(--text-muted-color, #bbb); -} diff --git a/UI/Web/src/app/_directives/dbl-click.directive.ts b/UI/Web/src/app/_directives/dbl-click.directive.ts deleted file mode 100644 index ab1d0bcde..000000000 --- a/UI/Web/src/app/_directives/dbl-click.directive.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Directive, EventEmitter, HostListener, Output} from '@angular/core'; - -@Directive({ - selector: '[appDblClick]', - standalone: true -}) -export class DblClickDirective { - - @Output() singleClick = new EventEmitter(); - @Output() doubleClick = new EventEmitter(); - - private lastTapTime = 0; - private tapTimeout = 300; // Time threshold for a double tap (in milliseconds) - private singleClickTimeout: any; - - @HostListener('click', ['$event']) - handleClick(event: Event): void { - const currentTime = new Date().getTime(); - - if (currentTime - this.lastTapTime < this.tapTimeout) { - // Detected a double click/tap - clearTimeout(this.singleClickTimeout); // Prevent single-click emission - event.stopPropagation(); - event.preventDefault(); - this.doubleClick.emit(event); - } else { - // Delay single-click emission to check if a double-click occurs - this.singleClickTimeout = setTimeout(() => { - this.singleClick.emit(event); // Optional: emit single-click if no double-click follows - }, this.tapTimeout); - } - - this.lastTapTime = currentTime; - } - -} diff --git a/UI/Web/src/app/_directives/enter-blur.directive.ts b/UI/Web/src/app/_directives/enter-blur.directive.ts deleted file mode 100644 index 30329f724..000000000 --- a/UI/Web/src/app/_directives/enter-blur.directive.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Directive, HostListener } from '@angular/core'; - -@Directive({ - selector: '[appEnterBlur]', - standalone: true, -}) -export class EnterBlurDirective { - @HostListener('keydown.enter', ['$event']) - onEnter(event: KeyboardEvent): void { - event.preventDefault(); - document.body.click(); - } -} diff --git a/UI/Web/src/app/_guards/admin.guard.ts b/UI/Web/src/app/_guards/admin.guard.ts index ade795609..9de2cfe44 100644 --- a/UI/Web/src/app/_guards/admin.guard.ts +++ b/UI/Web/src/app/_guards/admin.guard.ts @@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { AccountService } from '../_services/account.service'; -import {TranslocoService} from "@jsverse/transloco"; +import {TranslocoService} from "@ngneat/transloco"; @Injectable({ providedIn: 'root' @@ -22,7 +22,7 @@ export class AdminGuard implements CanActivate { } this.toastr.error(this.translocoService.translate('toasts.unauthorized-1')); - this.router.navigateByUrl('/home'); + this.router.navigateByUrl('/libraries'); return false; }) ); diff --git a/UI/Web/src/app/_guards/auth.guard.ts b/UI/Web/src/app/_guards/auth.guard.ts index 41a8b1eef..5d403469e 100644 --- a/UI/Web/src/app/_guards/auth.guard.ts +++ b/UI/Web/src/app/_guards/auth.guard.ts @@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { AccountService } from '../_services/account.service'; -import {TranslocoService} from "@jsverse/transloco"; +import {TranslocoService} from "@ngneat/transloco"; @Injectable({ providedIn: 'root' diff --git a/UI/Web/src/app/_helpers/browser.ts b/UI/Web/src/app/_helpers/browser.ts deleted file mode 100644 index 4d92e207c..000000000 --- a/UI/Web/src/app/_helpers/browser.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const isSafari = [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes("Mac") && "ontouchend" in document); - -/** - * Represents a Version for a browser - */ -export class Version { - major: number; - minor: number; - patch: number; - - constructor(major: number, minor: number, patch: number) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - isLessThan(other: Version): boolean { - if (this.major < other.major) return true; - if (this.major > other.major) return false; - if (this.minor < other.minor) return true; - if (this.minor > other.minor) return false; - return this.patch < other.patch; - } - - isGreaterThan(other: Version): boolean { - if (this.major > other.major) return true; - if (this.major < other.major) return false; - if (this.minor > other.minor) return true; - if (this.minor < other.minor) return false; - return this.patch > other.patch; - } - - isEqualTo(other: Version): boolean { - return ( - this.major === other.major && - this.minor === other.minor && - this.patch === other.patch - ); - } -} - - -export const getIosVersion = () => { - const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); - if (match) { - const major = parseInt(match[1], 10); - const minor = parseInt(match[2], 10); - const patch = parseInt(match[3] || '0', 10); - - return new Version(major, minor, patch); - } - return null; -} diff --git a/UI/Web/src/app/_helpers/form-debug.ts b/UI/Web/src/app/_helpers/form-debug.ts deleted file mode 100644 index 4ad70ac87..000000000 --- a/UI/Web/src/app/_helpers/form-debug.ts +++ /dev/null @@ -1,120 +0,0 @@ -import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; - -interface ValidationIssue { - path: string; - controlType: string; - value: any; - errors: { [key: string]: any } | null; - status: string; - disabled: boolean; -} - -export function analyzeFormGroupValidation(formGroup: FormGroup, basePath: string = ''): ValidationIssue[] { - const issues: ValidationIssue[] = []; - - function analyzeControl(control: AbstractControl, path: string): void { - // Determine control type for better debugging - let controlType = 'AbstractControl'; - if (control instanceof FormGroup) { - controlType = 'FormGroup'; - } else if (control instanceof FormArray) { - controlType = 'FormArray'; - } else if (control instanceof FormControl) { - controlType = 'FormControl'; - } - - // Add issue if control has validation errors or is invalid - if (control.invalid || control.errors || control.disabled) { - issues.push({ - path: path || 'root', - controlType, - value: control.value, - errors: control.errors, - status: control.status, - disabled: control.disabled - }); - } - - // Recursively check nested controls - if (control instanceof FormGroup) { - Object.keys(control.controls).forEach(key => { - const childPath = path ? `${path}.${key}` : key; - analyzeControl(control.controls[key], childPath); - }); - } else if (control instanceof FormArray) { - control.controls.forEach((childControl, index) => { - const childPath = path ? `${path}[${index}]` : `[${index}]`; - analyzeControl(childControl, childPath); - }); - } - } - - analyzeControl(formGroup, basePath); - return issues; -} - -export function printFormGroupValidation(formGroup: FormGroup, basePath: string = ''): void { - const issues = analyzeFormGroupValidation(formGroup, basePath); - - console.group(`🔍 FormGroup Validation Analysis (${basePath || 'root'})`); - console.log(`Overall Status: ${formGroup.status}`); - console.log(`Overall Valid: ${formGroup.valid}`); - console.log(`Total Issues Found: ${issues.length}`); - - if (issues.length === 0) { - console.log('✅ No validation issues found!'); - } else { - console.log('\n📋 Detailed Issues:'); - issues.forEach((issue, index) => { - console.group(`${index + 1}. ${issue.path} (${issue.controlType})`); - console.log(`Status: ${issue.status}`); - console.log(`Value:`, issue.value); - console.log(`Disabled: ${issue.disabled}`); - - if (issue.errors) { - console.log('Validation Errors:'); - Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { - console.log(` • ${errorKey}:`, errorValue); - }); - } else { - console.log('No specific validation errors (but control is invalid)'); - } - console.groupEnd(); - }); - } - - console.groupEnd(); -} - -// Alternative function that returns a formatted string instead of console logging -export function getFormGroupValidationReport(formGroup: FormGroup, basePath: string = ''): string { - const issues = analyzeFormGroupValidation(formGroup, basePath); - - let report = `FormGroup Validation Report (${basePath || 'root'})\n`; - report += `Overall Status: ${formGroup.status}\n`; - report += `Overall Valid: ${formGroup.valid}\n`; - report += `Total Issues Found: ${issues.length}\n\n`; - - if (issues.length === 0) { - report += '✅ No validation issues found!'; - } else { - report += 'Detailed Issues:\n'; - issues.forEach((issue, index) => { - report += `\n${index + 1}. ${issue.path} (${issue.controlType})\n`; - report += ` Status: ${issue.status}\n`; - report += ` Value: ${JSON.stringify(issue.value)}\n`; - report += ` Disabled: ${issue.disabled}\n`; - - if (issue.errors) { - report += ' Validation Errors:\n'; - Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { - report += ` • ${errorKey}: ${JSON.stringify(errorValue)}\n`; - }); - } else { - report += ' No specific validation errors (but control is invalid)\n'; - } - }); - } - - return report; -} diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index 503ca4516..77b27acf0 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -1,11 +1,16 @@ import {Injectable} from '@angular/core'; -import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { catchError } from 'rxjs/operators'; import { AccountService } from '../_services/account.service'; -import {translate, TranslocoService} from "@jsverse/transloco"; +import {translate, TranslocoService} from "@ngneat/transloco"; @Injectable() export class ErrorInterceptor implements HttpInterceptor { @@ -36,7 +41,7 @@ export class ErrorInterceptor implements HttpInterceptor { break; default: // Don't throw multiple Something unexpected went wrong - const genericError = translate('errors.generic'); + let genericError = translate('errors.generic'); if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.' && this.toastr.previousToastMessage !== genericError) { this.toast(genericError); } diff --git a/UI/Web/src/app/_interceptors/jwt.interceptor.ts b/UI/Web/src/app/_interceptors/jwt.interceptor.ts index 711b8ee11..c81a784e6 100644 --- a/UI/Web/src/app/_interceptors/jwt.interceptor.ts +++ b/UI/Web/src/app/_interceptors/jwt.interceptor.ts @@ -1,5 +1,10 @@ import {Injectable} from '@angular/core'; -import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; import {Observable, switchMap} from 'rxjs'; import { AccountService } from '../_services/account.service'; import { take } from 'rxjs/operators'; diff --git a/UI/Web/src/app/_models/auth/invite-user-response.ts b/UI/Web/src/app/_models/auth/invite-user-response.ts index 4a6e29dc6..a9042c555 100644 --- a/UI/Web/src/app/_models/auth/invite-user-response.ts +++ b/UI/Web/src/app/_models/auth/invite-user-response.ts @@ -7,8 +7,4 @@ export interface InviteUserResponse { * If an email was sent to the invited user */ emailSent: boolean; - /** - * When a user has an invalid email and is attempting to perform a flow. - */ - invalidEmail: boolean; -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/auth/member.ts b/UI/Web/src/app/_models/auth/member.ts index aaa45f332..a9b50d110 100644 --- a/UI/Web/src/app/_models/auth/member.ts +++ b/UI/Web/src/app/_models/auth/member.ts @@ -1,16 +1,14 @@ -import {AgeRestriction} from '../metadata/age-restriction'; -import {Library} from '../library/library'; +import { AgeRestriction } from '../metadata/age-restriction'; +import { Library } from '../library'; export interface Member { - id: number; - username: string; - email: string; - lastActive: string; // datetime - lastActiveUtc: string; // datetime - created: string; // datetime - createdUtc: string; // datetime - roles: string[]; - libraries: Library[]; - ageRestriction: AgeRestriction; - isPending: boolean; -} + id: number; + username: string; + email: string; + lastActive: string; // datetime + created: string; // datetime + roles: string[]; + libraries: Library[]; + ageRestriction: AgeRestriction; + isPending: boolean; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/auth/update-email-response.ts b/UI/Web/src/app/_models/auth/update-email-response.ts new file mode 100644 index 000000000..eaaf64580 --- /dev/null +++ b/UI/Web/src/app/_models/auth/update-email-response.ts @@ -0,0 +1,10 @@ +export interface UpdateEmailResponse { + /** + * Did the user not have an existing email + */ + hadNoExistingEmail: boolean; + /** + * Was an email sent (ie is this server accessible) + */ + emailSent: boolean; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/chapter-detail-plus.ts b/UI/Web/src/app/_models/chapter-detail-plus.ts deleted file mode 100644 index 2a17089e1..000000000 --- a/UI/Web/src/app/_models/chapter-detail-plus.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {UserReview} from "../_single-module/review-card/user-review"; -import {Rating} from "./rating"; - -export type ChapterDetailPlus = { - rating: number; - hasBeenRated: boolean; - reviews: UserReview[]; - ratings: Rating[]; -}; diff --git a/UI/Web/src/app/_models/chapter.ts b/UI/Web/src/app/_models/chapter.ts index e52efd202..f3010fb95 100644 --- a/UI/Web/src/app/_models/chapter.ts +++ b/UI/Web/src/app/_models/chapter.ts @@ -1,29 +1,13 @@ import { MangaFile } from './manga-file'; import { AgeRating } from './metadata/age-rating'; -import {PublicationStatus} from "./metadata/publication-status"; -import {Genre} from "./metadata/genre"; -import {Tag} from "./tag"; -import {Person} from "./metadata/person"; -import {IHasCast} from "./common/i-has-cast"; -import {IHasReadingTime} from "./common/i-has-reading-time"; -import {IHasCover} from "./common/i-has-cover"; -import {IHasProgress} from "./common/i-has-progress"; - -export const LooseLeafOrDefaultNumber = -100000; -export const SpecialVolumeNumber = 100000; /** * Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields. */ -export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgress { +export interface Chapter { id: number; range: string; - /** - * @deprecated Use minNumber/maxNumber - */ number: string; - minNumber: number; - maxNumber: number; files: Array; /** * This is used in the UI, it is not updated or sent to Backend @@ -58,53 +42,4 @@ export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgr webLinks: string; isbn: string; lastReadingProgress: string; - sortOrder: number; - - primaryColor: string; - secondaryColor: string; - - year: string; - language: string; - publicationStatus: PublicationStatus; - count: number; - totalCount: number; - - genres: Array; - tags: Array; - writers: Array; - coverArtists: Array; - publishers: Array; - characters: Array; - pencillers: Array; - inkers: Array; - imprints: Array; - colorists: Array; - letterers: Array; - editors: Array; - translators: Array; - teams: Array; - locations: Array; - - summaryLocked: boolean; - genresLocked: boolean; - tagsLocked: boolean; - writerLocked: boolean; - coverArtistLocked: boolean; - publisherLocked: boolean; - characterLocked: boolean; - pencillerLocked: boolean; - inkerLocked: boolean; - imprintLocked: boolean; - coloristLocked: boolean; - lettererLocked: boolean; - editorLocked: boolean; - translatorLocked: boolean; - teamLocked: boolean; - locationLocked: boolean; - ageRatingLocked: boolean; - languageLocked: boolean; - isbnLocked: boolean; - titleNameLocked: boolean; - sortOrderLocked: boolean; - releaseDateLocked: boolean; } diff --git a/UI/Web/src/app/_models/collection-tag.ts b/UI/Web/src/app/_models/collection-tag.ts index 2679b5f73..af0952f83 100644 --- a/UI/Web/src/app/_models/collection-tag.ts +++ b/UI/Web/src/app/_models/collection-tag.ts @@ -1,25 +1,11 @@ -import {ScrobbleProvider} from "../_services/scrobbling.service"; -import {AgeRating} from "./metadata/age-rating"; - -export interface UserCollection { - id: number; - title: string; - promoted: boolean; - /** - * This is used as a placeholder to store the coverImage url. The backend does not use this or send it. - */ - coverImage: string; - coverImageLocked: boolean; - summary: string; - lastSyncUtc: string; - owner: string; - source: ScrobbleProvider; - sourceUrl: string | null; - totalSourceCount: number; - /** - * HTML anchors separated by
- */ - missingSeriesFromSource: string | null; - ageRating: AgeRating; - itemCount: number; -} +export interface CollectionTag { + id: number; + title: string; + promoted: boolean; + /** + * This is used as a placeholder to store the coverImage url. The backend does not use this or send it. + */ + coverImage: string; + coverImageLocked: boolean; + summary: string; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/collection/mal-stack.ts b/UI/Web/src/app/_models/collection/mal-stack.ts deleted file mode 100644 index 5868a202d..000000000 --- a/UI/Web/src/app/_models/collection/mal-stack.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface MalStack { - title: string; - stackId: number; - url: string; - author?: string; - seriesCount: number; - restackCount: number; -} diff --git a/UI/Web/src/app/_models/common/i-has-cast.ts b/UI/Web/src/app/_models/common/i-has-cast.ts deleted file mode 100644 index 351352cb2..000000000 --- a/UI/Web/src/app/_models/common/i-has-cast.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Person} from "../metadata/person"; - -export interface IHasCast { - writerLocked: boolean; - coverArtistLocked: boolean; - publisherLocked: boolean; - characterLocked: boolean; - pencillerLocked: boolean; - inkerLocked: boolean; - imprintLocked: boolean; - coloristLocked: boolean; - lettererLocked: boolean; - editorLocked: boolean; - translatorLocked: boolean; - teamLocked: boolean; - locationLocked: boolean; - languageLocked: boolean; - - writers: Array; - coverArtists: Array; - publishers: Array; - characters: Array; - pencillers: Array; - inkers: Array; - imprints: Array; - colorists: Array; - letterers: Array; - editors: Array; - translators: Array; - teams: Array; - locations: Array; -} - -export function hasAnyCast(entity: IHasCast | null | undefined): boolean { - if (entity === null || entity === undefined) return false; - - return entity.writers.length > 0 || - entity.coverArtists.length > 0 || - entity.publishers.length > 0 || - entity.characters.length > 0 || - entity.pencillers.length > 0 || - entity.inkers.length > 0 || - entity.imprints.length > 0 || - entity.colorists.length > 0 || - entity.letterers.length > 0 || - entity.editors.length > 0 || - entity.translators.length > 0 || - entity.teams.length > 0 || - entity.locations.length > 0; -} diff --git a/UI/Web/src/app/_models/common/i-has-cover.ts b/UI/Web/src/app/_models/common/i-has-cover.ts deleted file mode 100644 index 7e58bbcbb..000000000 --- a/UI/Web/src/app/_models/common/i-has-cover.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IHasCover { - coverImage?: string; - primaryColor: string; - secondaryColor: string; -} diff --git a/UI/Web/src/app/_models/common/i-has-progress.ts b/UI/Web/src/app/_models/common/i-has-progress.ts deleted file mode 100644 index 4605dc0a8..000000000 --- a/UI/Web/src/app/_models/common/i-has-progress.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IHasProgress { - pages: number; - pagesRead: number; -} diff --git a/UI/Web/src/app/_models/common/i-has-reading-time.ts b/UI/Web/src/app/_models/common/i-has-reading-time.ts deleted file mode 100644 index 41753d1fd..000000000 --- a/UI/Web/src/app/_models/common/i-has-reading-time.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface IHasReadingTime { - minHoursToRead: number; - maxHoursToRead: number; - avgHoursToRead: number; - pages: number; - wordCount: number; - -} diff --git a/UI/Web/src/app/_models/default-modal-options.ts b/UI/Web/src/app/_models/default-modal-options.ts deleted file mode 100644 index 4dbb391a9..000000000 --- a/UI/Web/src/app/_models/default-modal-options.ts +++ /dev/null @@ -1 +0,0 @@ -export const DefaultModalOptions = {scrollable: true, size: 'xl', fullscreen: 'xl'}; diff --git a/UI/Web/src/app/_models/email-history.ts b/UI/Web/src/app/_models/email-history.ts deleted file mode 100644 index 0805704fb..000000000 --- a/UI/Web/src/app/_models/email-history.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface EmailHistory { - sent: boolean; - sendDate: string; - emailTemplate: string; - errorMessage: string; - toUserName: string; -} diff --git a/UI/Web/src/app/_models/events/chapter-removed-event.ts b/UI/Web/src/app/_models/events/chapter-removed-event.ts deleted file mode 100644 index 5413a1923..000000000 --- a/UI/Web/src/app/_models/events/chapter-removed-event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ChapterRemovedEvent { - chapterId: number; - seriesId: number; -} diff --git a/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts b/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts deleted file mode 100644 index 3695651d6..000000000 --- a/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ExternalMatchRateLimitErrorEvent { - seriesId: number; - seriesName: string; -} diff --git a/UI/Web/src/app/_models/events/site-theme-updated-event.ts b/UI/Web/src/app/_models/events/site-theme-updated-event.ts deleted file mode 100644 index fea80c979..000000000 --- a/UI/Web/src/app/_models/events/site-theme-updated-event.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface SiteThemeUpdatedEvent { - themeName: string; -} diff --git a/UI/Web/src/app/_models/events/update-version-event.ts b/UI/Web/src/app/_models/events/update-version-event.ts index 4e7e82ce6..d5845881c 100644 --- a/UI/Web/src/app/_models/events/update-version-event.ts +++ b/UI/Web/src/app/_models/events/update-version-event.ts @@ -1,26 +1,9 @@ export interface UpdateVersionEvent { - currentVersion: string; - updateVersion: string; - updateBody: string; - updateTitle: string; - updateUrl: string; - isDocker: boolean; - publishDate: string; - isOnNightlyInRelease: boolean; - isReleaseNewer: boolean; - isReleaseEqual: boolean; - - added: Array; - removed: Array; - changed: Array; - fixed: Array; - theme: Array; - developer: Array; - api: Array; - featureRequests: Array; - knownIssues: Array; - /** - * The part above the changelog part - */ - blogPart: string; -} + currentVersion: string; + updateVersion: string; + updateBody: string; + updateTitle: string; + updateUrl: string; + isDocker: boolean; + publishDate: string; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/events/volume-removed-event.ts b/UI/Web/src/app/_models/events/volume-removed-event.ts deleted file mode 100644 index 1ce2dc2ed..000000000 --- a/UI/Web/src/app/_models/events/volume-removed-event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface VolumeRemovedEvent { - volumeId: number; - seriesId: number; -} diff --git a/UI/Web/src/app/_models/kavitaplus/license-info.ts b/UI/Web/src/app/_models/kavitaplus/license-info.ts deleted file mode 100644 index 4a724b3ff..000000000 --- a/UI/Web/src/app/_models/kavitaplus/license-info.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface LicenseInfo { - expirationDate: string; - isActive: boolean; - isCancelled: boolean; - isValidVersion: boolean; - registeredEmail: string; - totalMonthsSubbed: number; - hasLicense: boolean; -} diff --git a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts b/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts deleted file mode 100644 index 05a4041c8..000000000 --- a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {MatchStateOption} from "./match-state-option"; -import {LibraryType} from "../library/library"; - -export interface ManageMatchFilter { - matchStateOption: MatchStateOption; - libraryType: LibraryType | -1; - searchTerm: string; -} diff --git a/UI/Web/src/app/_models/kavitaplus/manage-match-series.ts b/UI/Web/src/app/_models/kavitaplus/manage-match-series.ts deleted file mode 100644 index 4138279e6..000000000 --- a/UI/Web/src/app/_models/kavitaplus/manage-match-series.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Series} from "../series"; - -export interface ManageMatchSeries { - series: Series; - isMatched: boolean; - validUntilUtc: string; -} diff --git a/UI/Web/src/app/_models/kavitaplus/match-state-option.ts b/UI/Web/src/app/_models/kavitaplus/match-state-option.ts deleted file mode 100644 index a52c5efad..000000000 --- a/UI/Web/src/app/_models/kavitaplus/match-state-option.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum MatchStateOption { - All = 0, - Matched = 1, - NotMatched = 2, - Error = 3, - DontMatch = 4 -} - -export const allMatchStates = [ - MatchStateOption.Matched, MatchStateOption.NotMatched, MatchStateOption.Error, MatchStateOption.DontMatch -]; diff --git a/UI/Web/src/app/_models/kavitaplus/user-token-info.ts b/UI/Web/src/app/_models/kavitaplus/user-token-info.ts deleted file mode 100644 index 1dcab9c91..000000000 --- a/UI/Web/src/app/_models/kavitaplus/user-token-info.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface UserTokenInfo { - userId: number; - username: string; - isAniListTokenSet: boolean; - aniListValidUntilUtc: string; - isAniListTokenValid: boolean; - isMalTokenSet: boolean; -} diff --git a/UI/Web/src/app/_models/library.ts b/UI/Web/src/app/_models/library.ts new file mode 100644 index 000000000..5687038ba --- /dev/null +++ b/UI/Web/src/app/_models/library.ts @@ -0,0 +1,22 @@ +export enum LibraryType { + Manga = 0, + Comic = 1, + Book = 2, +} + +export interface Library { + id: number; + name: string; + lastScanned: string; + type: LibraryType; + folders: string[]; + coverImage?: string; + folderWatching: boolean; + includeInDashboard: boolean; + includeInRecommended: boolean; + includeInSearch: boolean; + manageCollections: boolean; + manageReadingLists: boolean; + allowScrobbling: boolean; + collapseSeriesRelationships: boolean; +} diff --git a/UI/Web/src/app/_models/library/file-type-group.enum.ts b/UI/Web/src/app/_models/library/file-type-group.enum.ts deleted file mode 100644 index a104782a5..000000000 --- a/UI/Web/src/app/_models/library/file-type-group.enum.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum FileTypeGroup { - Archive = 1, - Epub = 2, - Pdf = 3, - Images = 4 -} - -export const allFileTypeGroup = Object.keys(FileTypeGroup) -.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) -.map(key => parseInt(key, 10)) as FileTypeGroup[]; diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts deleted file mode 100644 index bcbf9b447..000000000 --- a/UI/Web/src/app/_models/library/library.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {FileTypeGroup} from "./file-type-group.enum"; - -export enum LibraryType { - Manga = 0, - Comic = 1, - Book = 2, - Images = 3, - LightNovel = 4, - /** - * Comic (Legacy) - */ - ComicVine = 5 -} - -export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images]; -export const allKavitaPlusMetadataApplicableTypes = [LibraryType.Manga, LibraryType.LightNovel, LibraryType.ComicVine, LibraryType.Comic]; -export const allKavitaPlusScrobbleEligibleTypes = [LibraryType.Manga, LibraryType.LightNovel]; - -export interface Library { - id: number; - name: string; - lastScanned: string; - type: LibraryType; - folders: string[]; - coverImage?: string | null; - folderWatching: boolean; - includeInDashboard: boolean; - includeInRecommended: boolean; - includeInSearch: boolean; - manageCollections: boolean; - manageReadingLists: boolean; - allowScrobbling: boolean; - allowMetadataMatching: boolean; - enableMetadata: boolean; - removePrefixForSortName: boolean; - collapseSeriesRelationships: boolean; - libraryFileTypes: Array; - excludePatterns: Array; -} diff --git a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts index fa3a5de99..26ee5e0bb 100644 --- a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts +++ b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts @@ -1,5 +1,5 @@ import { FileDimension } from "src/app/manga-reader/_models/file-dimension"; -import { LibraryType } from "../library/library"; +import { LibraryType } from "../library"; import { MangaFormat } from "../manga-format"; export interface BookmarkInfo { @@ -17,4 +17,4 @@ export interface BookmarkInfo { * This will not always be present. Depends on if asked from backend. */ doublePairs?: {[key: number]: number}; -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/browse/browse-genre.ts b/UI/Web/src/app/_models/metadata/browse/browse-genre.ts deleted file mode 100644 index e7bb0d915..000000000 --- a/UI/Web/src/app/_models/metadata/browse/browse-genre.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Genre} from "../genre"; - -export interface BrowseGenre extends Genre { - seriesCount: number; - chapterCount: number; -} diff --git a/UI/Web/src/app/_models/metadata/browse/browse-person.ts b/UI/Web/src/app/_models/metadata/browse/browse-person.ts deleted file mode 100644 index 886f9455b..000000000 --- a/UI/Web/src/app/_models/metadata/browse/browse-person.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Person} from "../person"; - -export interface BrowsePerson extends Person { - seriesCount: number; - chapterCount: number; -} diff --git a/UI/Web/src/app/_models/metadata/browse/browse-tag.ts b/UI/Web/src/app/_models/metadata/browse/browse-tag.ts deleted file mode 100644 index 4d87370ee..000000000 --- a/UI/Web/src/app/_models/metadata/browse/browse-tag.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Tag} from "../../tag"; - -export interface BrowseTag extends Tag { - seriesCount: number; - chapterCount: number; -} diff --git a/UI/Web/src/app/_models/metadata/chapter-metadata.ts b/UI/Web/src/app/_models/metadata/chapter-metadata.ts new file mode 100644 index 000000000..4606021a9 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/chapter-metadata.ts @@ -0,0 +1,39 @@ +import { Genre } from "./genre"; +import { AgeRating } from "./age-rating"; +import { PublicationStatus } from "./publication-status"; +import { Person } from "./person"; +import { Tag } from "../tag"; + +export interface ChapterMetadata { + id: number; + chapterId: number; + title: string; + year: string; + + ageRating: AgeRating; + releaseDate: string; + language: string; + publicationStatus: PublicationStatus; + summary: string; + count: number; + totalCount: number; + wordCount: number; + + + + genres: Array; + tags: Array; + writers: Array; + coverArtists: Array; + publishers: Array; + characters: Array; + pencillers: Array; + inkers: Array; + colorists: Array; + letterers: Array; + editors: Array; + translators: Array; + + + +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/language.ts b/UI/Web/src/app/_models/metadata/language.ts index 28ab2b598..e8f606bec 100644 --- a/UI/Web/src/app/_models/metadata/language.ts +++ b/UI/Web/src/app/_models/metadata/language.ts @@ -3,13 +3,3 @@ export interface Language { title: string; } -export interface KavitaLocale { - /** - * isoCode aka what maps to the file on disk and what transloco loads - */ - fileName: string; - renderName: string; - translationCompletion: number; - isRtL: boolean; - hash: string; -} diff --git a/UI/Web/src/app/_models/metadata/person.ts b/UI/Web/src/app/_models/metadata/person.ts index efc8df914..e23925cef 100644 --- a/UI/Web/src/app/_models/metadata/person.ts +++ b/UI/Web/src/app/_models/metadata/person.ts @@ -1,52 +1,20 @@ -import {IHasCover} from "../common/i-has-cover"; - export enum PersonRole { - Other = 1, - Writer = 3, - Penciller = 4, - Inker = 5, - Colorist = 6, - Letterer = 7, - CoverArtist = 8, - Editor = 9, - Publisher = 10, - Character = 11, - Translator = 12, - Imprint = 13, - Team = 14, - Location = 15 + Other = 1, + Artist = 2, + Writer = 3, + Penciller = 4, + Inker = 5, + Colorist = 6, + Letterer = 7, + CoverArtist = 8, + Editor = 9, + Publisher = 10, + Character = 11, + Translator = 12 } -export interface Person extends IHasCover { - id: number; - name: string; - description: string; - aliases: Array; - coverImage?: string; - coverImageLocked: boolean; - malId?: number; - aniListId?: number; - hardcoverId?: string; - asin?: string; - primaryColor: string; - secondaryColor: string; -} - -/** - * Excludes Other as it's not in use - */ -export const allPeopleRoles = [ - PersonRole.Writer, - PersonRole.Penciller, - PersonRole.Inker, - PersonRole.Colorist, - PersonRole.Letterer, - PersonRole.CoverArtist, - PersonRole.Editor, - PersonRole.Publisher, - PersonRole.Character, - PersonRole.Translator, - PersonRole.Imprint, - PersonRole.Team, - PersonRole.Location -] +export interface Person { + id: number; + name: string; + role: PersonRole; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 7875732b7..843844416 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -1,5 +1,6 @@ -import {MangaFormat} from "../manga-format"; -import {FilterV2} from "./v2/filter-v2"; +import { MangaFormat } from "../manga-format"; +import { SeriesFilterV2 } from "./v2/series-filter-v2"; +import {FilterField} from "./v2/filter-field"; export interface FilterItem { title: string; @@ -7,6 +8,10 @@ export interface FilterItem { selected: boolean; } +export interface SortOptions { + sortField: SortField; + isAscending: boolean; +} export enum SortField { SortName = 1, @@ -16,42 +21,37 @@ export enum SortField { TimeToRead = 5, ReleaseYear = 6, ReadProgress = 7, - /** - * Kavita+ only - */ - AverageRating = 8, - Random = 9 } -export const allSeriesSortFields = Object.keys(SortField) +export const allSortFields = Object.keys(SortField) .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) .map(key => parseInt(key, 10)) as SortField[]; export const mangaFormatFilters = [ { - title: 'images', + title: 'Images', value: MangaFormat.IMAGE, selected: false }, { - title: 'epub', + title: 'EPUB', value: MangaFormat.EPUB, selected: false }, { - title: 'pdf', + title: 'PDF', value: MangaFormat.PDF, selected: false }, { - title: 'archive', + title: 'ARCHIVE', value: MangaFormat.ARCHIVE, selected: false } ]; -export interface FilterEvent { - filterV2: FilterV2; +export interface FilterEvent { + filterV2: SeriesFilterV2; isFirst: boolean; } diff --git a/UI/Web/src/app/_models/metadata/series-metadata.ts b/UI/Web/src/app/_models/metadata/series-metadata.ts index fc691ee93..f32bf2838 100644 --- a/UI/Web/src/app/_models/metadata/series-metadata.ts +++ b/UI/Web/src/app/_models/metadata/series-metadata.ts @@ -1,17 +1,18 @@ +import { CollectionTag } from "../collection-tag"; import { Genre } from "./genre"; import { AgeRating } from "./age-rating"; import { PublicationStatus } from "./publication-status"; import { Person } from "./person"; import { Tag } from "../tag"; -import {IHasCast} from "../common/i-has-cast"; -export interface SeriesMetadata extends IHasCast { +export interface SeriesMetadata { seriesId: number; summary: string; totalCount: number; maxCount: number; + collectionTags: Array; genres: Array; tags: Array; writers: Array; @@ -20,13 +21,10 @@ export interface SeriesMetadata extends IHasCast { characters: Array; pencillers: Array; inkers: Array; - imprints: Array; colorists: Array; letterers: Array; editors: Array; translators: Array; - teams: Array; - locations: Array; ageRating: AgeRating; releaseYear: number; language: string; @@ -36,21 +34,18 @@ export interface SeriesMetadata extends IHasCast { summaryLocked: boolean; genresLocked: boolean; tagsLocked: boolean; - writerLocked: boolean; - coverArtistLocked: boolean; - publisherLocked: boolean; - characterLocked: boolean; - pencillerLocked: boolean; - inkerLocked: boolean; - imprintLocked: boolean; - coloristLocked: boolean; - lettererLocked: boolean; - editorLocked: boolean; - translatorLocked: boolean; - teamLocked: boolean; - locationLocked: boolean; + writersLocked: boolean; + coverArtistsLocked: boolean; + publishersLocked: boolean; + charactersLocked: boolean; + pencillersLocked: boolean; + inkersLocked: boolean; + coloristsLocked: boolean; + letterersLocked: boolean; + editorsLocked: boolean; + translatorsLocked: boolean; ageRatingLocked: boolean; releaseYearLocked: boolean; languageLocked: boolean; publicationStatusLocked: boolean; -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts b/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts deleted file mode 100644 index bb5edc9ce..000000000 --- a/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {PersonRole} from "../person"; -import {PersonSortOptions} from "./sort-options"; - -export interface BrowsePersonFilter { - roles: Array; - query?: string; - sortOptions?: PersonSortOptions; -} diff --git a/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts b/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts index 2dafc0e48..fa30dc786 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts @@ -43,5 +43,4 @@ export enum FilterComparison { /// Is Date not between now and X seconds ago ///
IsNotInLast = 15, - IsEmpty = 16 } diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index eeb8c7853..8544e5ccb 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -1,5 +1,3 @@ -import {PersonRole} from "../person"; - export enum FilterField { None = -1, @@ -30,12 +28,7 @@ export enum FilterField Path = 24, FilePath = 25, WantToRead = 26, - ReadingDate = 27, - AverageRating = 28, - Imprint = 29, - Team = 30, - Location = 31, - ReadLast = 32 + ReadingDate = 27 } @@ -48,37 +41,5 @@ const enumArray = Object.keys(FilterField) enumArray.sort((a, b) => a.value.localeCompare(b.value)); -export const allSeriesFilterFields = enumArray +export const allFields = enumArray .map(key => parseInt(key.key, 10))as FilterField[]; - -export const allPeople = [ - FilterField.Characters, - FilterField.Colorist, - FilterField.CoverArtist, - FilterField.Editor, - FilterField.Inker, - FilterField.Letterer, - FilterField.Penciller, - FilterField.Publisher, - FilterField.Translators, - FilterField.Writers, -]; - -export const personRoleForFilterField = (role: PersonRole) => { - switch (role) { - case PersonRole.Character: return FilterField.Characters; - case PersonRole.Colorist: return FilterField.Colorist; - case PersonRole.CoverArtist: return FilterField.CoverArtist; - case PersonRole.Editor: return FilterField.Editor; - case PersonRole.Inker: return FilterField.Inker; - case PersonRole.Letterer: return FilterField.Letterer; - case PersonRole.Penciller: return FilterField.Penciller; - case PersonRole.Publisher: return FilterField.Publisher; - case PersonRole.Translator: return FilterField.Translators; - case PersonRole.Writer: return FilterField.Writers; - case PersonRole.Imprint: return FilterField.Imprint; - case PersonRole.Location: return FilterField.Location; - case PersonRole.Team: return FilterField.Team; - case PersonRole.Other: return FilterField.None; - } -}; diff --git a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts index b14fe564d..d031927a2 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts @@ -1,7 +1,8 @@ -import {FilterComparison} from "./filter-comparison"; +import { FilterComparison } from "./filter-comparison"; +import { FilterField } from "./filter-field"; -export interface FilterStatement { +export interface FilterStatement { comparison: FilterComparison; - field: T; + field: FilterField; value: string; -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/v2/filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/filter-v2.ts deleted file mode 100644 index 77c064450..000000000 --- a/UI/Web/src/app/_models/metadata/v2/filter-v2.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {FilterStatement} from "./filter-statement"; -import {FilterCombination} from "./filter-combination"; -import {SortOptions} from "./sort-options"; - -export interface FilterV2 { - name?: string; - statements: Array>; - combination: FilterCombination; - sortOptions?: SortOptions; - limitTo: number; -} diff --git a/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts b/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts deleted file mode 100644 index 6bfb5a0c1..000000000 --- a/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum PersonFilterField { - Role = 1, - Name = 2, - SeriesCount = 3, - ChapterCount = 4, -} - - -export const allPersonFilterFields = Object.keys(PersonFilterField) - .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) - .map(key => parseInt(key, 10)) as PersonFilterField[]; - diff --git a/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts b/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts deleted file mode 100644 index 6bcb66925..000000000 --- a/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum PersonSortField { - Name = 1, - SeriesCount = 2, - ChapterCount = 3 -} - -export const allPersonSortFields = Object.keys(PersonSortField) - .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) - .map(key => parseInt(key, 10)) as PersonSortField[]; diff --git a/UI/Web/src/app/_models/metadata/v2/query-context.ts b/UI/Web/src/app/_models/metadata/v2/query-context.ts deleted file mode 100644 index 63a5c0032..000000000 --- a/UI/Web/src/app/_models/metadata/v2/query-context.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum QueryContext -{ - None = 1, - Search = 2, - Recommended = 3, - Dashboard = 4, -} diff --git a/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts new file mode 100644 index 000000000..c13244644 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts @@ -0,0 +1,11 @@ +import { SortOptions } from "../series-filter"; +import {FilterStatement} from "./filter-statement"; +import {FilterCombination} from "./filter-combination"; + +export interface SeriesFilterV2 { + name?: string; + statements: Array; + combination: FilterCombination; + sortOptions?: SortOptions; + limitTo: number; +} diff --git a/UI/Web/src/app/_models/metadata/v2/sort-options.ts b/UI/Web/src/app/_models/metadata/v2/sort-options.ts deleted file mode 100644 index ed68d6b9d..000000000 --- a/UI/Web/src/app/_models/metadata/v2/sort-options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {PersonSortField} from "./person-sort-field"; - -/** - * Series-based Sort options - */ -export interface SortOptions { - sortField: TSort; - isAscending: boolean; -} - -/** - * Person-based Sort Options - */ -export interface PersonSortOptions { - sortField: PersonSortField; - isAscending: boolean; -} diff --git a/UI/Web/src/app/_models/pagination.ts b/UI/Web/src/app/_models/pagination.ts index 8d6a4a06a..c007c528a 100644 --- a/UI/Web/src/app/_models/pagination.ts +++ b/UI/Web/src/app/_models/pagination.ts @@ -1,15 +1,8 @@ -export class Pagination { +export interface Pagination { currentPage: number; itemsPerPage: number; totalItems: number; totalPages: number; - - constructor() { - this.currentPage = 0; - this.itemsPerPage = 0; - this.totalItems = 0; - this.totalPages = 0; - } } export class PaginatedResult { diff --git a/UI/Web/src/app/_models/preferences/book-theme.ts b/UI/Web/src/app/_models/preferences/book-theme.ts index cb321c110..b6e37f6e4 100644 --- a/UI/Web/src/app/_models/preferences/book-theme.ts +++ b/UI/Web/src/app/_models/preferences/book-theme.ts @@ -1,7 +1,7 @@ -import {ThemeProvider} from "./site-theme"; +import { ThemeProvider } from "./site-theme"; /** - * Theme for the book reader contents + * Theme for the the book reader contents */ export interface BookTheme { name: string; diff --git a/UI/Web/src/app/_models/preferences/pdf-layout-mode.ts b/UI/Web/src/app/_models/preferences/pdf-layout-mode.ts deleted file mode 100644 index 53a54a851..000000000 --- a/UI/Web/src/app/_models/preferences/pdf-layout-mode.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum PdfLayoutMode { - Multiple = 0, - Single = 1, - Book = 2, - InfiniteScroll = 3 -} diff --git a/UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts b/UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts deleted file mode 100644 index 2a122590d..000000000 --- a/UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum PdfScrollMode { - Vertical = 0, - Horizontal = 1, - Wrapped = 2, - Page = 3 -} diff --git a/UI/Web/src/app/_models/preferences/pdf-spread-mode.ts b/UI/Web/src/app/_models/preferences/pdf-spread-mode.ts deleted file mode 100644 index 7bd437add..000000000 --- a/UI/Web/src/app/_models/preferences/pdf-spread-mode.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum PdfSpreadMode { - None = 0, - Odd = 1, - Even = 2 -} diff --git a/UI/Web/src/app/_models/preferences/pdf-theme.ts b/UI/Web/src/app/_models/preferences/pdf-theme.ts deleted file mode 100644 index b3ecc1796..000000000 --- a/UI/Web/src/app/_models/preferences/pdf-theme.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum PdfTheme{ - Dark = 0, - Light = 1 -} diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 886c570e2..83b7907a8 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -1,20 +1,55 @@ -import {PageLayoutMode} from '../page-layout-mode'; -import {SiteTheme} from './site-theme'; + +import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode'; +import { BookPageLayoutMode } from '../readers/book-page-layout-mode'; +import { PageLayoutMode } from '../page-layout-mode'; +import { PageSplitOption } from './page-split-option'; +import { ReaderMode } from './reader-mode'; +import { ReadingDirection } from './reading-direction'; +import { ScalingOption } from './scaling-option'; +import { SiteTheme } from './site-theme'; +import {WritingStyle} from "./writing-style"; export interface Preferences { + // Manga Reader + readingDirection: ReadingDirection; + scalingOption: ScalingOption; + pageSplitOption: PageSplitOption; + readerMode: ReaderMode; + autoCloseMenu: boolean; + layoutMode: LayoutMode; + backgroundColor: string; + showScreenHints: boolean; + emulateBook: boolean; + swipeToPaginate: boolean; - // Global - theme: SiteTheme; - globalPageLayoutMode: PageLayoutMode; - blurUnreadSummaries: boolean; - promptForDownloadSize: boolean; - noTransitions: boolean; - collapseSeriesRelationships: boolean; - shareReviews: boolean; - locale: string; + // Book Reader + bookReaderMargin: number; + bookReaderLineSpacing: number; + bookReaderFontSize: number; + bookReaderFontFamily: string; + bookReaderTapToPaginate: boolean; + bookReaderReadingDirection: ReadingDirection; + bookReaderWritingStyle: WritingStyle; + bookReaderThemeName: string; + bookReaderLayoutMode: BookPageLayoutMode; + bookReaderImmersiveMode: boolean; - // Kavita+ - aniListScrobblingEnabled: boolean; - wantToReadSync: boolean; + // Global + theme: SiteTheme; + globalPageLayoutMode: PageLayoutMode; + blurUnreadSummaries: boolean; + promptForDownloadSize: boolean; + noTransitions: boolean; + collapseSeriesRelationships: boolean; + shareReviews: boolean; + locale: string; } +export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}]; +export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}]; +export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; +export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; +export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; +export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} +export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; +export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; diff --git a/UI/Web/src/app/_models/preferences/reading-profiles.ts b/UI/Web/src/app/_models/preferences/reading-profiles.ts deleted file mode 100644 index dad02946f..000000000 --- a/UI/Web/src/app/_models/preferences/reading-profiles.ts +++ /dev/null @@ -1,80 +0,0 @@ -import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode'; -import {BookPageLayoutMode} from '../readers/book-page-layout-mode'; -import {PageLayoutMode} from '../page-layout-mode'; -import {PageSplitOption} from './page-split-option'; -import {ReaderMode} from './reader-mode'; -import {ReadingDirection} from './reading-direction'; -import {ScalingOption} from './scaling-option'; -import {WritingStyle} from "./writing-style"; -import {PdfTheme} from "./pdf-theme"; -import {PdfScrollMode} from "./pdf-scroll-mode"; -import {PdfLayoutMode} from "./pdf-layout-mode"; -import {PdfSpreadMode} from "./pdf-spread-mode"; -import {Series} from "../series"; -import {Library} from "../library/library"; -import {UserBreakpoint} from "../../shared/_services/utility.service"; - -export enum ReadingProfileKind { - Default = 0, - User = 1, - Implicit = 2, -} - -export interface ReadingProfile { - - id: number; - name: string; - normalizedName: string; - kind: ReadingProfileKind; - - // Manga Reader - readingDirection: ReadingDirection; - scalingOption: ScalingOption; - pageSplitOption: PageSplitOption; - readerMode: ReaderMode; - autoCloseMenu: boolean; - layoutMode: LayoutMode; - backgroundColor: string; - showScreenHints: boolean; - emulateBook: boolean; - swipeToPaginate: boolean; - allowAutomaticWebtoonReaderDetection: boolean; - widthOverride?: number; - disableWidthOverride: UserBreakpoint; - - // Book Reader - bookReaderMargin: number; - bookReaderLineSpacing: number; - bookReaderFontSize: number; - bookReaderFontFamily: string; - bookReaderTapToPaginate: boolean; - bookReaderReadingDirection: ReadingDirection; - bookReaderWritingStyle: WritingStyle; - bookReaderThemeName: string; - bookReaderLayoutMode: BookPageLayoutMode; - bookReaderImmersiveMode: boolean; - - // PDF Reader - pdfTheme: PdfTheme; - pdfScrollMode: PdfScrollMode; - pdfSpreadMode: PdfSpreadMode; - - // relations - seriesIds: number[]; - libraryIds: number[]; - -} - -export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}]; -export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}]; -export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; -export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; -export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; -export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} -export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; -export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; -export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}]; -export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}]; -export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}]; -export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}]; -export const breakPoints = [UserBreakpoint.Never, UserBreakpoint.Mobile, UserBreakpoint.Tablet, UserBreakpoint.Desktop] diff --git a/UI/Web/src/app/_models/preferences/site-theme.ts b/UI/Web/src/app/_models/preferences/site-theme.ts index a861a5a11..675d4dad3 100644 --- a/UI/Web/src/app/_models/preferences/site-theme.ts +++ b/UI/Web/src/app/_models/preferences/site-theme.ts @@ -3,9 +3,9 @@ */ export enum ThemeProvider { System = 1, - Custom = 2, + User = 2 } - + /** * Theme for the whole instance */ @@ -20,8 +20,4 @@ * The actual class the root is defined against. It is generated at the backend. */ selector: string; - description: string; - previewUrls: Array; - author: string; - - } + } \ No newline at end of file diff --git a/UI/Web/src/app/_models/rating.ts b/UI/Web/src/app/_models/rating.ts index 7132706f9..a4c4b79ed 100644 --- a/UI/Web/src/app/_models/rating.ts +++ b/UI/Web/src/app/_models/rating.ts @@ -1,15 +1,9 @@ import {ScrobbleProvider} from "../_services/scrobbling.service"; -export enum RatingAuthority { - User = 0, - Critic = 1, -} - export interface Rating { averageScore: number; meanScore: number; favoriteCount: number; provider: ScrobbleProvider; providerUrl: string | undefined; - authority: RatingAuthority; } diff --git a/UI/Web/src/app/_models/readers/full-progress.ts b/UI/Web/src/app/_models/readers/full-progress.ts deleted file mode 100644 index 2b34be267..000000000 --- a/UI/Web/src/app/_models/readers/full-progress.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface FullProgress { - id: number; - chapterId: number; - pagesRead: number; - lastModified: string; - lastModifiedUtc: string; - created: string; - createdUtc: string; - appUserId: number; - userName: string; -} diff --git a/UI/Web/src/app/_models/reading-list.ts b/UI/Web/src/app/_models/reading-list.ts index 646360153..daff4f57b 100644 --- a/UI/Web/src/app/_models/reading-list.ts +++ b/UI/Web/src/app/_models/reading-list.ts @@ -1,57 +1,37 @@ -import {LibraryType} from "./library/library"; -import {MangaFormat} from "./manga-format"; -import {IHasCover} from "./common/i-has-cover"; -import {AgeRating} from "./metadata/age-rating"; -import {IHasReadingTime} from "./common/i-has-reading-time"; -import {IHasCast} from "./common/i-has-cast"; +import { LibraryType } from "./library"; +import { MangaFormat } from "./manga-format"; export interface ReadingListItem { - pagesRead: number; - pagesTotal: number; - seriesName: string; - seriesFormat: MangaFormat; - seriesId: number; - chapterId: number; - order: number; - chapterNumber: string; - volumeNumber: string; - libraryId: number; - id: number; - releaseDate: string; - title: string; - libraryType: LibraryType; - libraryName: string; - summary?: string; + pagesRead: number; + pagesTotal: number; + seriesName: string; + seriesFormat: MangaFormat; + seriesId: number; + chapterId: number; + order: number; + chapterNumber: string; + volumeNumber: string; + libraryId: number; + id: number; + releaseDate: string; + title: string; + libraryType: LibraryType; + libraryName: string; } -export interface ReadingList extends IHasCover { - id: number; - title: string; - summary: string; - promoted: boolean; - coverImageLocked: boolean; - items: Array; - /** - * If this is empty or null, the cover image isn't set. Do not use this externally. - */ - coverImage?: string; - primaryColor: string; - secondaryColor: string; - startingYear: number; - startingMonth: number; - endingYear: number; - endingMonth: number; - itemCount: number; - ageRating: AgeRating; -} - -export interface ReadingListInfo extends IHasReadingTime, IHasReadingTime { - pages: number; - wordCount: number; - isAllEpub: boolean; - minHoursToRead: number; - maxHoursToRead: number; - avgHoursToRead: number; -} - -export interface ReadingListCast extends IHasCast {} +export interface ReadingList { + id: number; + title: string; + summary: string; + promoted: boolean; + coverImageLocked: boolean; + items: Array; + /** + * If this is empty or null, the cover image isn't set. Do not use this externally. + */ + coverImage: string; + startingYear: number; + startingMonth: number; + endingYear: number; + endingMonth: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/recently-added-item.ts b/UI/Web/src/app/_models/recently-added-item.ts index b1cc6e8eb..4c44474a8 100644 --- a/UI/Web/src/app/_models/recently-added-item.ts +++ b/UI/Web/src/app/_models/recently-added-item.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "./library/library"; +import { LibraryType } from "./library"; export interface RecentlyAddedItem { seriesId: number; @@ -8,6 +8,6 @@ export interface RecentlyAddedItem { libraryId: number; libraryType: LibraryType; volumeId: number; - chapterId: number; + chapterId: number; id: number; // This is UI only, sent from backend but has no relation to any entity -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/scrobbling/scrobble-event-filter.ts b/UI/Web/src/app/_models/scrobbling/scrobble-event-filter.ts index c0ea95d64..102cf89d1 100644 --- a/UI/Web/src/app/_models/scrobbling/scrobble-event-filter.ts +++ b/UI/Web/src/app/_models/scrobbling/scrobble-event-filter.ts @@ -4,8 +4,7 @@ export enum ScrobbleEventSortField { LastModified = 2, Type= 3, Series = 4, - IsProcessed = 5, - ScrobbleEvent = 6 + IsProcessed = 5 } export interface ScrobbleEventFilter { diff --git a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts index 7db1ceeaa..41113e5d0 100644 --- a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts +++ b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts @@ -7,7 +7,6 @@ export enum ScrobbleEventType { } export interface ScrobbleEvent { - id: number; seriesName: string; seriesId: number; libraryId: number; @@ -19,10 +18,4 @@ export interface ScrobbleEvent { createdUtc: string; volumeNumber: number | null; chapterNumber: number | null; - isErrored: boolean; - /** - * Null when not errored - */ - errorDetails: string | null; - } diff --git a/UI/Web/src/app/_models/search/bookmark-search-result.ts b/UI/Web/src/app/_models/search/bookmark-search-result.ts deleted file mode 100644 index 726772f73..000000000 --- a/UI/Web/src/app/_models/search/bookmark-search-result.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface BookmarkSearchResult { - libraryId: number; - seriesId: number; - volumeId: number; - chapterId: number; - seriesName: string; - localizedSeriesName: string; -} diff --git a/UI/Web/src/app/_models/search/search-result-group.ts b/UI/Web/src/app/_models/search/search-result-group.ts index 7391cdad9..a9d0005dd 100644 --- a/UI/Web/src/app/_models/search/search-result-group.ts +++ b/UI/Web/src/app/_models/search/search-result-group.ts @@ -1,25 +1,19 @@ import { Chapter } from "../chapter"; -import { Library } from "../library/library"; +import { Library } from "../library"; import { MangaFile } from "../manga-file"; import { SearchResult } from "./search-result"; import { Tag } from "../tag"; -import {BookmarkSearchResult} from "./bookmark-search-result"; -import {Genre} from "../metadata/genre"; -import {ReadingList} from "../reading-list"; -import {UserCollection} from "../collection-tag"; -import {Person} from "../metadata/person"; export class SearchResultGroup { libraries: Array = []; series: Array = []; - collections: Array = []; - readingLists: Array = []; - persons: Array = []; - genres: Array = []; + collections: Array = []; + readingLists: Array = []; + persons: Array = []; + genres: Array = []; tags: Array = []; files: Array = []; chapters: Array = []; - bookmarks: Array = []; reset() { this.libraries = []; @@ -30,7 +24,6 @@ export class SearchResultGroup { this.genres = []; this.tags = []; this.files = []; - this.chapters = []; - this.bookmarks = []; + this.chapters = []; } -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/series-detail/external-series-detail.ts b/UI/Web/src/app/_models/series-detail/external-series-detail.ts index db25782ca..1bd008292 100644 --- a/UI/Web/src/app/_models/series-detail/external-series-detail.ts +++ b/UI/Web/src/app/_models/series-detail/external-series-detail.ts @@ -1,5 +1,3 @@ -import {ScrobbleProvider} from "../../_services/scrobbling.service"; - export enum PlusMediaFormat { Manga = 1, Comic = 2, @@ -27,9 +25,8 @@ export interface MetadataTagDto { export interface ExternalSeriesDetail { name: string; - aniListId?: number | null; - malId?: number | null; - cbrId?: number | null; + aniListId?: number; + malId?: number; synonyms: Array; plusMediaFormat: PlusMediaFormat; siteUrl?: string; @@ -38,12 +35,7 @@ export interface ExternalSeriesDetail { summary?: string; volumeCount?: number; chapterCount?: number; - /** - * These are duplicated with volumeCount based on where it's being invoked. - */ - volumes?: number; - chapters?: number; staff: Array; tags: Array; - provider: ScrobbleProvider; + } diff --git a/UI/Web/src/app/_models/series-detail/external-series-match.ts b/UI/Web/src/app/_models/series-detail/external-series-match.ts deleted file mode 100644 index 28afea18a..000000000 --- a/UI/Web/src/app/_models/series-detail/external-series-match.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {ExternalSeriesDetail} from "./external-series-detail"; - -export interface ExternalSeriesMatch { - series: ExternalSeriesDetail; - matchRating: number; -} diff --git a/UI/Web/src/app/_models/series-detail/external-series.ts b/UI/Web/src/app/_models/series-detail/external-series.ts index 8bf95331f..1d0a46bc5 100644 --- a/UI/Web/src/app/_models/series-detail/external-series.ts +++ b/UI/Web/src/app/_models/series-detail/external-series.ts @@ -1,5 +1,3 @@ -import {ScrobbleProvider} from "../../_services/scrobbling.service"; - export interface ExternalSeries { name: string; coverUrl: string; @@ -7,5 +5,4 @@ export interface ExternalSeries { summary: string; aniListId?: number; malId?: number; - provider: ScrobbleProvider; } diff --git a/UI/Web/src/app/_models/series-detail/hour-estimate-range.ts b/UI/Web/src/app/_models/series-detail/hour-estimate-range.ts index 805a71178..f94ac569b 100644 --- a/UI/Web/src/app/_models/series-detail/hour-estimate-range.ts +++ b/UI/Web/src/app/_models/series-detail/hour-estimate-range.ts @@ -1,5 +1,6 @@ -export interface HourEstimateRange { +export interface HourEstimateRange{ minHours: number; maxHours: number; avgHours: number; -} + //hasProgress: boolean; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/series-detail/related-series.ts b/UI/Web/src/app/_models/series-detail/related-series.ts index aa24138bd..f0cfc230b 100644 --- a/UI/Web/src/app/_models/series-detail/related-series.ts +++ b/UI/Web/src/app/_models/series-detail/related-series.ts @@ -15,5 +15,4 @@ export interface RelatedSeries { doujinshis: Array; parent: Array; editions: Array; - annuals: Array; } diff --git a/UI/Web/src/app/_models/series-detail/relation-kind.ts b/UI/Web/src/app/_models/series-detail/relation-kind.ts index 9417e61da..77470041c 100644 --- a/UI/Web/src/app/_models/series-detail/relation-kind.ts +++ b/UI/Web/src/app/_models/series-detail/relation-kind.ts @@ -14,16 +14,14 @@ export enum RelationKind { * This is UI only. Backend will generate Parent series for everything but Prequel/Sequel */ Parent = 12, - Edition = 13, - Annual = 14 + Edition = 13 } -const RelationKindsUnsorted = [ +export const RelationKinds = [ {text: 'Prequel', value: RelationKind.Prequel}, {text: 'Sequel', value: RelationKind.Sequel}, {text: 'Spin Off', value: RelationKind.SpinOff}, {text: 'Adaptation', value: RelationKind.Adaptation}, - {text: 'Annual', value: RelationKind.Annual}, {text: 'Alternative Setting', value: RelationKind.AlternativeSetting}, {text: 'Alternative Version', value: RelationKind.AlternativeVersion}, {text: 'Side Story', value: RelationKind.SideStory}, @@ -33,5 +31,3 @@ const RelationKindsUnsorted = [ {text: 'Doujinshi', value: RelationKind.Doujinshi}, {text: 'Other', value: RelationKind.Other}, ]; - -export const RelationKinds = RelationKindsUnsorted.slice().sort((a, b) => a.text.localeCompare(b.text)); diff --git a/UI/Web/src/app/_models/series-detail/series-detail-plus.ts b/UI/Web/src/app/_models/series-detail/series-detail-plus.ts deleted file mode 100644 index 8160b210f..000000000 --- a/UI/Web/src/app/_models/series-detail/series-detail-plus.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Recommendation} from "./recommendation"; -import {UserReview} from "../../_single-module/review-card/user-review"; -import {Rating} from "../rating"; - -export interface SeriesDetailPlus { - recommendations?: Recommendation; - reviews: Array; - ratings?: Array; -} diff --git a/UI/Web/src/app/_models/series-group.ts b/UI/Web/src/app/_models/series-group.ts index 76c09d135..657890c25 100644 --- a/UI/Web/src/app/_models/series-group.ts +++ b/UI/Web/src/app/_models/series-group.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "./library/library"; +import { LibraryType } from "./library"; export interface SeriesGroup { seriesId: number; @@ -8,7 +8,7 @@ export interface SeriesGroup { libraryId: number; libraryType: LibraryType; volumeId: number; - chapterId: number; + chapterId: number; id: number; // This is UI only, sent from backend but has no relation to any entity - count: number; -} + count: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/series.ts b/UI/Web/src/app/_models/series.ts index 29d4aed7f..c994a3527 100644 --- a/UI/Web/src/app/_models/series.ts +++ b/UI/Web/src/app/_models/series.ts @@ -1,82 +1,67 @@ import { MangaFormat } from './manga-format'; import { Volume } from './volume'; -import {IHasCover} from "./common/i-has-cover"; -import {IHasReadingTime} from "./common/i-has-reading-time"; -import {IHasProgress} from "./common/i-has-progress"; -export interface Series extends IHasCover, IHasReadingTime, IHasProgress { - id: number; - name: string; - /** - * This is not shown to user - */ - originalName: string; - localizedName: string; - sortName: string; - coverImageLocked: boolean; - sortNameLocked: boolean; - localizedNameLocked: boolean; - nameLocked: boolean; - volumes: Volume[]; - /** - * Total pages in series - */ - pages: number; - /** - * Total pages the logged in user has read - */ - pagesRead: number; - /** - * User's rating (0-5) - */ - userRating: number; - hasUserRated: boolean; - libraryId: number; - /** - * DateTime the entity was created - */ - created: string; - /** - * Format of the Series - */ - format: MangaFormat; - /** - * DateTime that represents last time the logged in user read this series - */ - latestReadDate: string; - /** - * DateTime representing last time a chapter was added to the Series - */ - lastChapterAdded: string; - /** - * DateTime representing last time the series folder was scanned - */ - lastFolderScanned: string; - /** - * Number of words in the series - */ - wordCount: number; - minHoursToRead: number; - maxHoursToRead: number; - avgHoursToRead: number; - /** - * Highest level folder containing this series - */ - folderPath: string; - lowestFolderPath: string; +export interface Series { + id: number; + name: string; + /** + * This is not shown to user + */ + originalName: string; + localizedName: string; + sortName: string; + coverImageLocked: boolean; + sortNameLocked: boolean; + localizedNameLocked: boolean; + nameLocked: boolean; + volumes: Volume[]; + /** + * Total pages in series + */ + pages: number; + /** + * Total pages the logged in user has read + */ + pagesRead: number; + /** + * User's rating (0-5) + */ + userRating: number; + hasUserRated: boolean; + libraryId: number; + /** + * DateTime the entity was created + */ + created: string; + /** + * Format of the Series + */ + format: MangaFormat; + /** + * DateTime that represents last time the logged in user read this series + */ + latestReadDate: string; + /** + * DateTime representing last time a chapter was added to the Series + */ + lastChapterAdded: string; + /** + * DateTime representing last time the series folder was scanned + */ + lastFolderScanned: string; + /** + * Number of words in the series + */ + wordCount: number; + minHoursToRead: number; + maxHoursToRead: number; + avgHoursToRead: number; + /** + * Highest level folder containing this series + */ + folderPath: string; /** * This is currently only used on Series detail page for recommendations */ summary?: string; - coverImage?: string; - primaryColor: string; - secondaryColor: string; - /** - * Kavita+ only. Will not perform any matching from Kavita+ - */ - dontMatch: boolean; - /** - * Kavita+ only. Did this series not match and won't without manual match - */ - isBlacklisted: boolean; } diff --git a/UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts b/UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts index 2d6ef4e4c..a294f9696 100644 --- a/UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts +++ b/UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts @@ -7,5 +7,4 @@ export enum SideNavStreamType { ExternalSource = 6, AllSeries = 7, WantToRead = 8, - BrowseAuthors = 9 } diff --git a/UI/Web/src/app/_models/sidenav/sidenav-stream.ts b/UI/Web/src/app/_models/sidenav/sidenav-stream.ts index 192bf73bd..7dd672f50 100644 --- a/UI/Web/src/app/_models/sidenav/sidenav-stream.ts +++ b/UI/Web/src/app/_models/sidenav/sidenav-stream.ts @@ -1,5 +1,5 @@ import {SideNavStreamType} from "./sidenav-stream-type.enum"; -import {Library} from "../library/library"; +import {Library, LibraryType} from "../library"; import {CommonStream} from "../common-stream"; import {ExternalSource} from "./external-source"; diff --git a/UI/Web/src/app/_models/standalone-chapter.ts b/UI/Web/src/app/_models/standalone-chapter.ts deleted file mode 100644 index 9d640ad81..000000000 --- a/UI/Web/src/app/_models/standalone-chapter.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Chapter} from "./chapter"; -import {LibraryType} from "./library/library"; - -export interface StandaloneChapter extends Chapter { - seriesId: number; - libraryId: number; - libraryType: LibraryType; - volumeTitle?: string; -} diff --git a/UI/Web/src/app/_models/theme/colorscape.ts b/UI/Web/src/app/_models/theme/colorscape.ts deleted file mode 100644 index 1fbd436fe..000000000 --- a/UI/Web/src/app/_models/theme/colorscape.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ColorScape { - primary?: string; - secondary?: string; -} diff --git a/UI/Web/src/app/_models/theme/downloadable-site-theme.ts b/UI/Web/src/app/_models/theme/downloadable-site-theme.ts deleted file mode 100644 index 62885c063..000000000 --- a/UI/Web/src/app/_models/theme/downloadable-site-theme.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface DownloadableSiteTheme { - name: string; - cssUrl: string; - previewUrls: Array; - author: string; - isCompatible: boolean; - lastCompatibleVersion: string; - alreadyDownloaded: boolean; - description: string; -} diff --git a/UI/Web/src/app/_models/user.ts b/UI/Web/src/app/_models/user.ts index c94a9485d..e5a74dbcf 100644 --- a/UI/Web/src/app/_models/user.ts +++ b/UI/Web/src/app/_models/user.ts @@ -1,16 +1,14 @@ -import {AgeRestriction} from './metadata/age-restriction'; -import {Preferences} from './preferences/preferences'; +import { AgeRestriction } from './metadata/age-restriction'; +import { Preferences } from './preferences/preferences'; // This interface is only used for login and storing/retrieving JWT from local storage export interface User { - username: string; - token: string; - refreshToken: string; - roles: string[]; - preferences: Preferences; - apiKey: string; - email: string; - ageRestriction: AgeRestriction; - hasRunScrobbleEventGeneration: boolean; - scrobbleEventGenerationRan: string; // datetime + username: string; + token: string; + refreshToken: string; + roles: string[]; + preferences: Preferences; + apiKey: string; + email: string; + ageRestriction: AgeRestriction; } diff --git a/UI/Web/src/app/_models/volume.ts b/UI/Web/src/app/_models/volume.ts index fa4a7989b..48da5a510 100644 --- a/UI/Web/src/app/_models/volume.ts +++ b/UI/Web/src/app/_models/volume.ts @@ -1,19 +1,14 @@ import { Chapter } from './chapter'; import { HourEstimateRange } from './series-detail/hour-estimate-range'; -import {IHasCover} from "./common/i-has-cover"; -import {IHasReadingTime} from "./common/i-has-reading-time"; -import {IHasProgress} from "./common/i-has-progress"; -export interface Volume extends IHasCover, IHasReadingTime, IHasProgress { +export interface Volume { id: number; - minNumber: number; - maxNumber: number; + number: number; name: string; createdUtc: string; lastModifiedUtc: string; pages: number; pagesRead: number; - wordCount: number; chapters: Array; /** * This is only available on the object when fetched for SeriesDetail @@ -22,9 +17,4 @@ export interface Volume extends IHasCover, IHasReadingTime, IHasProgress { minHoursToRead: number; maxHoursToRead: number; avgHoursToRead: number; - - coverImage?: string; - coverImageLocked: boolean; - primaryColor: string; - secondaryColor: string; } diff --git a/UI/Web/src/app/_models/wiki.ts b/UI/Web/src/app/_models/wiki.ts deleted file mode 100644 index a01267cf3..000000000 --- a/UI/Web/src/app/_models/wiki.ts +++ /dev/null @@ -1,25 +0,0 @@ -export enum WikiLink { - Customize = 'https://wiki.kavitareader.com/guides/features/customization', - CustomizeExternalSource = 'https://wiki.kavitareader.com/guides/features/customization#external-source', - ReadingLists = 'https://wiki.kavitareader.com/guides/features/readinglists', - Collections = 'https://wiki.kavitareader.com/guides/features/collections', - SeriesRelationships = 'https://wiki.kavitareader.com/guides/features/relationships', - Bookmarks = 'https://wiki.kavitareader.com/guides/features/bookmarks', - DataCollection = 'https://wiki.kavitareader.com/troubleshooting/faq#q-does-kavita-collect-any-data-on-me', - MediaIssues = 'https://wiki.kavitareader.com/guides/admin-settings/mediaissues/', - KavitaPlusDiscordId = 'https://wiki.kavitareader.com/guides/admin-settings/kavita+#discord-id', - KavitaPlus = 'https://wiki.kavitareader.com/kavita+', - KavitaPlusFAQ = 'https://wiki.kavitareader.com/kavita+/faq', - ReadingListCBL = 'https://wiki.kavitareader.com/guides/features/readinglists#creating-a-reading-list-via-cbl', - Donation = 'https://wiki.kavitareader.com/donating', - Updating = 'https://wiki.kavitareader.com/guides/updating', - ManagingFiles = 'https://wiki.kavitareader.com/guides/scanner/managefiles', - Scanner = 'https://wiki.kavitareader.com/guides/scanner', - ScannerExclude = 'https://wiki.kavitareader.com/guides/admin-settings/libraries#exclude-patterns', - Library = 'https://wiki.kavitareader.com/guides/admin-settings/libraries', - UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native', - UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker', - OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients', - Guides = 'https://wiki.kavitareader.com/guides', - ReadingProfiles = "https://wiki.kavitareader.com/guides/user-settings/reading-profiles/", -} diff --git a/UI/Web/src/app/_pipes/book-page-layout-mode.pipe.ts b/UI/Web/src/app/_pipes/book-page-layout-mode.pipe.ts deleted file mode 100644 index 41758552f..000000000 --- a/UI/Web/src/app/_pipes/book-page-layout-mode.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {BookPageLayoutMode} from "../_models/readers/book-page-layout-mode"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'bookPageLayoutMode', - standalone: true -}) -export class BookPageLayoutModePipe implements PipeTransform { - - transform(value: BookPageLayoutMode): string { - const v = parseInt(value + '', 10) as BookPageLayoutMode; - switch (v) { - case BookPageLayoutMode.Column1: return translate('preferences.1-column'); - case BookPageLayoutMode.Column2: return translate('preferences.2-column'); - case BookPageLayoutMode.Default: return translate('preferences.scroll'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/breakpoint.pipe.ts b/UI/Web/src/app/_pipes/breakpoint.pipe.ts deleted file mode 100644 index 1897b773c..000000000 --- a/UI/Web/src/app/_pipes/breakpoint.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {UserBreakpoint} from "../shared/_services/utility.service"; - -@Pipe({ - name: 'breakpoint' -}) -export class BreakpointPipe implements PipeTransform { - - transform(value: UserBreakpoint): string { - const v = parseInt(value + '', 10) as UserBreakpoint; - switch (v) { - case UserBreakpoint.Never: - return translate('breakpoint-pipe.never'); - case UserBreakpoint.Mobile: - return translate('breakpoint-pipe.mobile'); - case UserBreakpoint.Tablet: - return translate('breakpoint-pipe.tablet'); - case UserBreakpoint.Desktop: - return translate('breakpoint-pipe.desktop'); - } - throw new Error("unknown breakpoint value: " + value); - } - -} diff --git a/UI/Web/src/app/_pipes/browse-title.pipe.ts b/UI/Web/src/app/_pipes/browse-title.pipe.ts deleted file mode 100644 index 0495e8b8a..000000000 --- a/UI/Web/src/app/_pipes/browse-title.pipe.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {FilterField} from "../_models/metadata/v2/filter-field"; -import {translate} from "@jsverse/transloco"; - -/** - * Responsible for taking a filter field and value (as a string) and translating into a "Browse X" heading for All Series page - * Example: Genre & "Action" -> Browse Action - * Example: Artist & "Joe Shmo" -> Browse Joe Shmo Works - */ -@Pipe({ - name: 'browseTitle' -}) -export class BrowseTitlePipe implements PipeTransform { - - transform(field: FilterField, value: string): string { - switch (field) { - case FilterField.PublicationStatus: - return translate('browse-title-pipe.publication-status', {value}); - case FilterField.AgeRating: - return translate('browse-title-pipe.age-rating', {value}); - case FilterField.UserRating: - return translate('browse-title-pipe.user-rating', {value}); - case FilterField.Tags: - return translate('browse-title-pipe.tag', {value}); - case FilterField.Translators: - return translate('browse-title-pipe.translator', {value}); - case FilterField.Characters: - return translate('browse-title-pipe.character', {value}); - case FilterField.Publisher: - return translate('browse-title-pipe.publisher', {value}); - case FilterField.Editor: - return translate('browse-title-pipe.editor', {value}); - case FilterField.CoverArtist: - return translate('browse-title-pipe.artist', {value}); - case FilterField.Letterer: - return translate('browse-title-pipe.letterer', {value}); - case FilterField.Colorist: - return translate('browse-title-pipe.colorist', {value}); - case FilterField.Inker: - return translate('browse-title-pipe.inker', {value}); - case FilterField.Penciller: - return translate('browse-title-pipe.penciller', {value}); - case FilterField.Writers: - return translate('browse-title-pipe.writer', {value}); - case FilterField.Genres: - return translate('browse-title-pipe.genre', {value}); - case FilterField.Libraries: - return translate('browse-title-pipe.library', {value}); - case FilterField.Formats: - return translate('browse-title-pipe.format', {value}); - case FilterField.ReleaseYear: - return translate('browse-title-pipe.release-year', {value}); - case FilterField.Imprint: - return translate('browse-title-pipe.imprint', {value}); - case FilterField.Team: - return translate('browse-title-pipe.team', {value}); - case FilterField.Location: - return translate('browse-title-pipe.location', {value}); - - // These have no natural links in the app to demand a richer title experience - case FilterField.Languages: - case FilterField.CollectionTags: - case FilterField.ReadProgress: - case FilterField.ReadTime: - case FilterField.Path: - case FilterField.FilePath: - case FilterField.WantToRead: - case FilterField.ReadingDate: - case FilterField.AverageRating: - case FilterField.ReadLast: - case FilterField.Summary: - case FilterField.SeriesName: - default: - return ''; - } - } - -} diff --git a/UI/Web/src/app/_pipes/confirm-translate.pipe.ts b/UI/Web/src/app/_pipes/confirm-translate.pipe.ts deleted file mode 100644 index 008f28849..000000000 --- a/UI/Web/src/app/_pipes/confirm-translate.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { translate } from '@jsverse/transloco'; - -@Pipe({ - name: 'confirmTranslate', - standalone: true -}) -export class ConfirmTranslatePipe implements PipeTransform { - - transform(value: string | undefined | null): string | undefined | null { - if (!value) return value; - - if (value.startsWith('confirm.')) { - return translate(value); - } - - return value; - } - -} diff --git a/UI/Web/src/app/_pipes/cover-image-size.pipe.ts b/UI/Web/src/app/_pipes/cover-image-size.pipe.ts deleted file mode 100644 index 8f54e269e..000000000 --- a/UI/Web/src/app/_pipes/cover-image-size.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {CoverImageSize} from "../admin/_models/cover-image-size"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'coverImageSize', - standalone: true -}) -export class CoverImageSizePipe implements PipeTransform { - - transform(value: CoverImageSize): string { - switch (value) { - case CoverImageSize.Default: - return translate('cover-image-size.default'); - case CoverImageSize.Medium: - return translate('cover-image-size.medium'); - case CoverImageSize.Large: - return translate('cover-image-size.large'); - case CoverImageSize.XLarge: - return translate('cover-image-size.xlarge'); - - } - } - -} diff --git a/UI/Web/src/app/_pipes/encode-format.pipe.ts b/UI/Web/src/app/_pipes/encode-format.pipe.ts deleted file mode 100644 index f082c0495..000000000 --- a/UI/Web/src/app/_pipes/encode-format.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {EncodeFormat} from "../admin/_models/encode-format"; - -@Pipe({ - name: 'encodeFormat', - standalone: true -}) -export class EncodeFormatPipe implements PipeTransform { - - transform(value: EncodeFormat): string { - switch (value) { - case EncodeFormat.PNG: - return 'PNG'; - case EncodeFormat.WebP: - return 'WebP'; - case EncodeFormat.AVIF: - return 'AVIF'; - } - } - -} diff --git a/UI/Web/src/app/_pipes/file-type-group.pipe.ts b/UI/Web/src/app/_pipes/file-type-group.pipe.ts deleted file mode 100644 index 88d595420..000000000 --- a/UI/Web/src/app/_pipes/file-type-group.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {FileTypeGroup} from "../_models/library/file-type-group.enum"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'fileTypeGroup', - standalone: true -}) -export class FileTypeGroupPipe implements PipeTransform { - - transform(value: FileTypeGroup): string { - switch (value) { - case FileTypeGroup.Archive: - return translate('file-type-group-pipe.archive'); - case FileTypeGroup.Epub: - return translate('file-type-group-pipe.epub'); - case FileTypeGroup.Pdf: - return translate('file-type-group-pipe.pdf'); - case FileTypeGroup.Images: - return translate('file-type-group-pipe.image'); - - } - } - -} diff --git a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts deleted file mode 100644 index f342c0034..000000000 --- a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {FilterField} from "../_models/metadata/v2/filter-field"; -import {translate} from "@jsverse/transloco"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; - -@Pipe({ - name: 'genericFilterField' -}) -export class GenericFilterFieldPipe implements PipeTransform { - - transform(value: T, entityType: ValidFilterEntity): string { - - switch (entityType) { - case "series": - return this.translateFilterField(value as FilterField); - case "person": - return this.translatePersonFilterField(value as PersonFilterField); - } - } - - private translatePersonFilterField(value: PersonFilterField) { - switch (value) { - case PersonFilterField.Role: - return translate('generic-filter-field-pipe.person-role'); - case PersonFilterField.Name: - return translate('generic-filter-field-pipe.person-name'); - case PersonFilterField.SeriesCount: - return translate('generic-filter-field-pipe.person-series-count'); - case PersonFilterField.ChapterCount: - return translate('generic-filter-field-pipe.person-chapter-count'); - } - } - - private translateFilterField(value: FilterField) { - switch (value) { - case FilterField.AgeRating: - return translate('filter-field-pipe.age-rating'); - case FilterField.Characters: - return translate('filter-field-pipe.characters'); - case FilterField.CollectionTags: - return translate('filter-field-pipe.collection-tags'); - case FilterField.Colorist: - return translate('filter-field-pipe.colorist'); - case FilterField.CoverArtist: - return translate('filter-field-pipe.cover-artist'); - case FilterField.Editor: - return translate('filter-field-pipe.editor'); - case FilterField.Formats: - return translate('filter-field-pipe.formats'); - case FilterField.Genres: - return translate('filter-field-pipe.genres'); - case FilterField.Inker: - return translate('filter-field-pipe.inker'); - case FilterField.Imprint: - return translate('filter-field-pipe.imprint'); - case FilterField.Team: - return translate('filter-field-pipe.team'); - case FilterField.Location: - return translate('filter-field-pipe.location'); - case FilterField.Languages: - return translate('filter-field-pipe.languages'); - case FilterField.Libraries: - return translate('filter-field-pipe.libraries'); - case FilterField.Letterer: - return translate('filter-field-pipe.letterer'); - case FilterField.PublicationStatus: - return translate('filter-field-pipe.publication-status'); - case FilterField.Penciller: - return translate('filter-field-pipe.penciller'); - case FilterField.Publisher: - return translate('filter-field-pipe.publisher'); - case FilterField.ReadProgress: - return translate('filter-field-pipe.read-progress'); - case FilterField.ReadTime: - return translate('filter-field-pipe.read-time'); - case FilterField.ReleaseYear: - return translate('filter-field-pipe.release-year'); - case FilterField.SeriesName: - return translate('filter-field-pipe.series-name'); - case FilterField.Summary: - return translate('filter-field-pipe.summary'); - case FilterField.Tags: - return translate('filter-field-pipe.tags'); - case FilterField.Translators: - return translate('filter-field-pipe.translators'); - case FilterField.UserRating: - return translate('filter-field-pipe.user-rating'); - case FilterField.Writers: - return translate('filter-field-pipe.writers'); - case FilterField.Path: - return translate('filter-field-pipe.path'); - case FilterField.FilePath: - return translate('filter-field-pipe.file-path'); - case FilterField.WantToRead: - return translate('filter-field-pipe.want-to-read'); - case FilterField.ReadingDate: - return translate('filter-field-pipe.read-date'); - case FilterField.ReadLast: - return translate('filter-field-pipe.read-last'); - case FilterField.AverageRating: - return translate('filter-field-pipe.average-rating'); - default: - throw new Error(`Invalid FilterField value: ${value}`); - } - } - -} diff --git a/UI/Web/src/app/_pipes/language-name.pipe.ts b/UI/Web/src/app/_pipes/language-name.pipe.ts deleted file mode 100644 index 697554bd3..000000000 --- a/UI/Web/src/app/_pipes/language-name.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { map, Observable } from 'rxjs'; -import { MetadataService } from '../_services/metadata.service'; -import {shareReplay} from "rxjs/operators"; - -@Pipe({ - name: 'languageName', - standalone: true -}) -export class LanguageNamePipe implements PipeTransform { - - constructor(private metadataService: MetadataService) {} - - transform(isoCode: string): Observable { - return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay()); - } - -} diff --git a/UI/Web/src/app/_pipes/layout-mode.pipe.ts b/UI/Web/src/app/_pipes/layout-mode.pipe.ts deleted file mode 100644 index ab598a7f4..000000000 --- a/UI/Web/src/app/_pipes/layout-mode.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {LayoutMode} from "../manga-reader/_models/layout-mode"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'layoutMode', - standalone: true -}) -export class LayoutModePipe implements PipeTransform { - - transform(value: LayoutMode): string { - const v = parseInt(value + '', 10) as LayoutMode; - switch (v) { - case LayoutMode.Single: return translate('preferences.single'); - case LayoutMode.Double: return translate('preferences.double'); - case LayoutMode.DoubleReversed: return translate('preferences.double-manga'); - case LayoutMode.DoubleNoCover: return translate('preferences.double-no-cover'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/library-name.pipe.ts b/UI/Web/src/app/_pipes/library-name.pipe.ts deleted file mode 100644 index c34f50166..000000000 --- a/UI/Web/src/app/_pipes/library-name.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {inject, Pipe, PipeTransform} from '@angular/core'; -import {LibraryService} from "../_services/library.service"; -import {Observable} from "rxjs"; - -@Pipe({ - name: 'libraryName', - standalone: true -}) -export class LibraryNamePipe implements PipeTransform { - private readonly libraryService = inject(LibraryService); - - transform(libraryId: number): Observable { - return this.libraryService.getLibraryName(libraryId); - } - -} diff --git a/UI/Web/src/app/_pipes/log-level.pipe.ts b/UI/Web/src/app/_pipes/log-level.pipe.ts deleted file mode 100644 index 1a1c7c19a..000000000 --- a/UI/Web/src/app/_pipes/log-level.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {translate} from "@jsverse/transloco"; - -/** - * Transforms the log level string into a localized string - */ -@Pipe({ - name: 'logLevel', - standalone: true, - pure: true -}) -export class LogLevelPipe implements PipeTransform { - transform(value: string): string { - return translate('log-level-pipe.' + value.toLowerCase()); - } - -} diff --git a/UI/Web/src/app/_pipes/match-state.pipe.ts b/UI/Web/src/app/_pipes/match-state.pipe.ts deleted file mode 100644 index 9f0cb00ae..000000000 --- a/UI/Web/src/app/_pipes/match-state.pipe.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {MatchStateOption} from "../_models/kavitaplus/match-state-option"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'matchStateOption', - standalone: true -}) -export class MatchStateOptionPipe implements PipeTransform { - - transform(value: MatchStateOption): string { - switch (value) { - case MatchStateOption.DontMatch: - return translate('manage-matched-metadata.dont-match-label'); - case MatchStateOption.All: - return translate('manage-matched-metadata.all-status-label'); - case MatchStateOption.Matched: - return translate('manage-matched-metadata.matched-status-label'); - case MatchStateOption.NotMatched: - return translate('manage-matched-metadata.unmatched-status-label'); - case MatchStateOption.Error: - return translate('manage-matched-metadata.blacklist-status-label'); - - } - } - -} diff --git a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts b/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts deleted file mode 100644 index dcaed4f69..000000000 --- a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {MetadataSettingField} from "../admin/_models/metadata-setting-field"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'metadataSettingFiled', - standalone: true -}) -export class MetadataSettingFiledPipe implements PipeTransform { - - transform(value: MetadataSettingField): string { - switch (value) { - case MetadataSettingField.ChapterTitle: - return translate('metadata-setting-field-pipe.chapter-title'); - case MetadataSettingField.ChapterSummary: - return translate('metadata-setting-field-pipe.chapter-summary'); - case MetadataSettingField.ChapterReleaseDate: - return translate('metadata-setting-field-pipe.chapter-release-date'); - case MetadataSettingField.ChapterPublisher: - return translate('metadata-setting-field-pipe.chapter-publisher'); - case MetadataSettingField.ChapterCovers: - return translate('metadata-setting-field-pipe.chapter-covers'); - case MetadataSettingField.AgeRating: - return translate('metadata-setting-field-pipe.age-rating'); - case MetadataSettingField.People: - return translate('metadata-setting-field-pipe.people'); - case MetadataSettingField.Covers: - return translate('metadata-setting-field-pipe.covers'); - case MetadataSettingField.Summary: - return translate('metadata-setting-field-pipe.summary'); - case MetadataSettingField.PublicationStatus: - return translate('metadata-setting-field-pipe.publication-status'); - case MetadataSettingField.StartDate: - return translate('metadata-setting-field-pipe.start-date'); - case MetadataSettingField.Genres: - return translate('metadata-setting-field-pipe.genres'); - case MetadataSettingField.Tags: - return translate('metadata-setting-field-pipe.tags'); - case MetadataSettingField.LocalizedName: - return translate('metadata-setting-field-pipe.localized-name'); - - } - } - -} diff --git a/UI/Web/src/app/_pipes/page-layout-mode.pipe.ts b/UI/Web/src/app/_pipes/page-layout-mode.pipe.ts deleted file mode 100644 index 45928d66b..000000000 --- a/UI/Web/src/app/_pipes/page-layout-mode.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {PageLayoutMode} from "../_models/page-layout-mode"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'pageLayoutMode', - standalone: true -}) -export class PageLayoutModePipe implements PipeTransform { - - transform(value: PageLayoutMode): string { - switch (value) { - case PageLayoutMode.Cards: return translate('preferences.cards'); - case PageLayoutMode.List: return translate('preferences.list'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/page-split-option.pipe.ts b/UI/Web/src/app/_pipes/page-split-option.pipe.ts deleted file mode 100644 index da6251f72..000000000 --- a/UI/Web/src/app/_pipes/page-split-option.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {PageSplitOption} from "../_models/preferences/page-split-option"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'pageSplitOption', - standalone: true -}) -export class PageSplitOptionPipe implements PipeTransform { - - transform(value: PageSplitOption): string { - const v = parseInt(value + '', 10) as PageSplitOption; - switch (v) { - case PageSplitOption.FitSplit: return translate('preferences.fit-to-screen'); - case PageSplitOption.NoSplit: return translate('preferences.no-split'); - case PageSplitOption.SplitLeftToRight: return translate('preferences.split-left-to-right'); - case PageSplitOption.SplitRightToLeft: return translate('preferences.split-right-to-left'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/pdf-scroll-mode.pipe.ts b/UI/Web/src/app/_pipes/pdf-scroll-mode.pipe.ts deleted file mode 100644 index d395911d6..000000000 --- a/UI/Web/src/app/_pipes/pdf-scroll-mode.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {PdfScrollMode} from "../_models/preferences/pdf-scroll-mode"; - -@Pipe({ - name: 'pdfScrollMode', - standalone: true -}) -export class PdfScrollModePipe implements PipeTransform { - - transform(value: PdfScrollMode): string { - const v = parseInt(value + '', 10) as PdfScrollMode; - switch (v) { - case PdfScrollMode.Wrapped: return translate('preferences.pdf-multiple'); - case PdfScrollMode.Page: return translate('preferences.pdf-page'); - case PdfScrollMode.Horizontal: return translate('preferences.pdf-horizontal'); - case PdfScrollMode.Vertical: return translate('preferences.pdf-vertical'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/pdf-spread-mode.pipe.ts b/UI/Web/src/app/_pipes/pdf-spread-mode.pipe.ts deleted file mode 100644 index 2f02363a3..000000000 --- a/UI/Web/src/app/_pipes/pdf-spread-mode.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {PdfSpreadMode} from "../_models/preferences/pdf-spread-mode"; -import {translate} from "@jsverse/transloco"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'pdfSpreadMode', - standalone: true -}) -export class PdfSpreadModePipe implements PipeTransform { - - transform(value: PdfSpreadMode): string { - const v = parseInt(value + '', 10) as PdfSpreadMode; - switch (v) { - case PdfSpreadMode.None: return translate('preferences.pdf-none'); - case PdfSpreadMode.Odd: return translate('preferences.pdf-odd'); - case PdfSpreadMode.Even: return translate('preferences.pdf-even'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/pdf-theme.pipe.ts b/UI/Web/src/app/_pipes/pdf-theme.pipe.ts deleted file mode 100644 index 4e64d85e8..000000000 --- a/UI/Web/src/app/_pipes/pdf-theme.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {PdfTheme} from "../_models/preferences/pdf-theme"; -import {translate} from "@jsverse/transloco"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'pdfTheme', - standalone: true -}) -export class PdfThemePipe implements PipeTransform { - - transform(value: PdfTheme): string { - const v = parseInt(value + '', 10) as PdfTheme; - switch (v) { - case PdfTheme.Dark: return translate('preferences.pdf-dark'); - case PdfTheme.Light: return translate('preferences.pdf-light'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/person-role.pipe.ts b/UI/Web/src/app/_pipes/person-role.pipe.ts deleted file mode 100644 index 1b9ee2163..000000000 --- a/UI/Web/src/app/_pipes/person-role.pipe.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {PersonRole} from '../_models/metadata/person'; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'personRole', - standalone: true -}) -export class PersonRolePipe implements PipeTransform { - - transform(value: PersonRole): string { - switch (value) { - case PersonRole.Character: - return translate('person-role-pipe.character'); - case PersonRole.Colorist: - return translate('person-role-pipe.colorist'); - case PersonRole.CoverArtist: - return translate('person-role-pipe.artist'); - case PersonRole.Editor: - return translate('person-role-pipe.editor'); - case PersonRole.Inker: - return translate('person-role-pipe.inker'); - case PersonRole.Letterer: - return translate('person-role-pipe.letterer'); - case PersonRole.Penciller: - return translate('person-role-pipe.penciller'); - case PersonRole.Publisher: - return translate('person-role-pipe.publisher'); - case PersonRole.Imprint: - return translate('person-role-pipe.imprint'); - case PersonRole.Writer: - return translate('person-role-pipe.writer'); - case PersonRole.Team: - return translate('person-role-pipe.team'); - case PersonRole.Location: - return translate('person-role-pipe.location'); - case PersonRole.Translator: - return translate('person-role-pipe.translator'); - case PersonRole.Other: - return translate('person-role-pipe.other'); - default: - return ''; - } - } - -} diff --git a/UI/Web/src/app/_pipes/plus-media-format.pipe.ts b/UI/Web/src/app/_pipes/plus-media-format.pipe.ts deleted file mode 100644 index b72822e33..000000000 --- a/UI/Web/src/app/_pipes/plus-media-format.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {PlusMediaFormat} from "../_models/series-detail/external-series-detail"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'plusMediaFormat', - standalone: true -}) -export class PlusMediaFormatPipe implements PipeTransform { - - transform(value: PlusMediaFormat): string { - switch (value) { - case PlusMediaFormat.Manga: - return translate('library-type-pipe.manga'); - case PlusMediaFormat.Comic: - return translate('library-type-pipe.comicVine'); - case PlusMediaFormat.LightNovel: - return translate('library-type-pipe.lightNovel'); - case PlusMediaFormat.Book: - return translate('library-type-pipe.book'); - - } - } - -} diff --git a/UI/Web/src/app/_pipes/provider-image.pipe.ts b/UI/Web/src/app/_pipes/provider-image.pipe.ts deleted file mode 100644 index 5d845a672..000000000 --- a/UI/Web/src/app/_pipes/provider-image.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {ScrobbleProvider} from "../_services/scrobbling.service"; - -@Pipe({ - name: 'providerImage', - standalone: true -}) -export class ProviderImagePipe implements PipeTransform { - - transform(value: ScrobbleProvider, large: boolean = false): string { - switch (value) { - case ScrobbleProvider.AniList: - return `assets/images/ExternalServices/AniList${large ? '-lg' : ''}.png`; - case ScrobbleProvider.Mal: - return `assets/images/ExternalServices/MAL${large ? '-lg' : ''}.png`; - case ScrobbleProvider.GoogleBooks: - return `assets/images/ExternalServices/GoogleBooks${large ? '-lg' : ''}.png`; - case ScrobbleProvider.Kavita: - return `assets/images/logo-${large ? '64' : '32'}.png`; - case ScrobbleProvider.Cbr: - return `assets/images/ExternalServices/ComicBookRoundup.png`; - } - } - -} diff --git a/UI/Web/src/app/_pipes/read-time-left.pipe.ts b/UI/Web/src/app/_pipes/read-time-left.pipe.ts deleted file mode 100644 index 43ac41c86..000000000 --- a/UI/Web/src/app/_pipes/read-time-left.pipe.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {TranslocoService} from "@jsverse/transloco"; -import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range"; -import {DecimalPipe} from "@angular/common"; - -@Pipe({ - name: 'readTimeLeft', - standalone: true -}) -export class ReadTimeLeftPipe implements PipeTransform { - - constructor(private readonly translocoService: TranslocoService) {} - - transform(readingTimeLeft: HourEstimateRange): string { - const hoursLabel = readingTimeLeft.avgHours > 1 - ? this.translocoService.translate('read-time-pipe.hours') - : this.translocoService.translate('read-time-pipe.hour'); - - const formattedHours = this.customRound(readingTimeLeft.avgHours); - - return `~${formattedHours} ${hoursLabel}`; - } - - private customRound(value: number): string { - const integerPart = Math.floor(value); - const decimalPart = value - integerPart; - - if (decimalPart < 0.5) { - // Round down to the nearest whole number - return integerPart.toString(); - } else if (decimalPart >= 0.5 && decimalPart < 0.9) { - // Return with 1 decimal place - return value.toFixed(1); - } else { - // Round up to the nearest whole number - return Math.ceil(value).toString(); - } - } -} diff --git a/UI/Web/src/app/_pipes/read-time.pipe.ts b/UI/Web/src/app/_pipes/read-time.pipe.ts deleted file mode 100644 index 1970b2812..000000000 --- a/UI/Web/src/app/_pipes/read-time.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {IHasReadingTime} from "../_models/common/i-has-reading-time"; -import {TranslocoService} from "@jsverse/transloco"; - -@Pipe({ - name: 'readTime', - standalone: true -}) -export class ReadTimePipe implements PipeTransform { - constructor(private translocoService: TranslocoService) {} - - transform(readingTime: IHasReadingTime): string { - if (readingTime.maxHoursToRead === 0 || readingTime.minHoursToRead === 0) { - return this.translocoService.translate('read-time-pipe.less-than-hour'); - } else { - return `${readingTime.minHoursToRead}${readingTime.maxHoursToRead !== readingTime.minHoursToRead ? ('-' + readingTime.maxHoursToRead) : ''}` + - ` ${readingTime.minHoursToRead > 1 ? this.translocoService.translate('read-time-pipe.hours') : this.translocoService.translate('read-time-pipe.hour')}`; - } - } - -} diff --git a/UI/Web/src/app/_pipes/reading-direction.pipe.ts b/UI/Web/src/app/_pipes/reading-direction.pipe.ts deleted file mode 100644 index d9a4097b0..000000000 --- a/UI/Web/src/app/_pipes/reading-direction.pipe.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {ReadingDirection} from "../_models/preferences/reading-direction"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'readingDirection', - standalone: true -}) -export class ReadingDirectionPipe implements PipeTransform { - - transform(value: ReadingDirection): string { - const v = parseInt(value + '', 10) as ReadingDirection; - switch (v) { - case ReadingDirection.LeftToRight: return translate('preferences.left-to-right'); - case ReadingDirection.RightToLeft: return translate('preferences.right-to-left'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/reading-mode.pipe.ts b/UI/Web/src/app/_pipes/reading-mode.pipe.ts deleted file mode 100644 index 0404e0ffb..000000000 --- a/UI/Web/src/app/_pipes/reading-mode.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {ReaderMode} from "../_models/preferences/reader-mode"; -import {translate} from "@jsverse/transloco"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'readerMode', - standalone: true -}) -export class ReaderModePipe implements PipeTransform { - - transform(value: ReaderMode): string { - const v = parseInt(value + '', 10) as ReaderMode; - switch (v) { - case ReaderMode.UpDown: return translate('preferences.up-to-down'); - case ReaderMode.Webtoon: return translate('preferences.webtoon'); - case ReaderMode.LeftRight: return translate('preferences.left-to-right'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/role-localized.pipe.ts b/UI/Web/src/app/_pipes/role-localized.pipe.ts deleted file mode 100644 index 1890962dd..000000000 --- a/UI/Web/src/app/_pipes/role-localized.pipe.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {Role} from "../_services/account.service"; -import {translate} from "@jsverse/transloco"; - -@Pipe({ - name: 'roleLocalized' -}) -export class RoleLocalizedPipe implements PipeTransform { - - transform(value: Role | string): string { - const key = (value + '').toLowerCase().replace(' ', '-'); - return translate(`role-localized-pipe.${key}`); - } - -} diff --git a/UI/Web/src/app/_pipes/safe-url.pipe.ts b/UI/Web/src/app/_pipes/safe-url.pipe.ts deleted file mode 100644 index fed4ae2d8..000000000 --- a/UI/Web/src/app/_pipes/safe-url.pipe.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { inject } from '@angular/core'; -import { Pipe, PipeTransform, SecurityContext } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; - -@Pipe({ - name: 'safeUrl', - pure: true, - standalone: true -}) -export class SafeUrlPipe implements PipeTransform { - private readonly dom: DomSanitizer = inject(DomSanitizer); - constructor() {} - - transform(value: string | null | undefined): string | null { - if (value === null || value === undefined) return null; - return this.dom.sanitize(SecurityContext.URL, value); - } - -} diff --git a/UI/Web/src/app/_pipes/scaling-option.pipe.ts b/UI/Web/src/app/_pipes/scaling-option.pipe.ts deleted file mode 100644 index d2124be7f..000000000 --- a/UI/Web/src/app/_pipes/scaling-option.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {ScalingOption} from "../_models/preferences/scaling-option"; -import {ReadingDirection} from "../_models/preferences/reading-direction"; - -@Pipe({ - name: 'scalingOption', - standalone: true -}) -export class ScalingOptionPipe implements PipeTransform { - - transform(value: ScalingOption): string { - const v = parseInt(value + '', 10) as ScalingOption; - switch (v) { - case ScalingOption.Automatic: return translate('preferences.automatic'); - case ScalingOption.FitToHeight: return translate('preferences.fit-to-height'); - case ScalingOption.FitToWidth: return translate('preferences.fit-to-width'); - case ScalingOption.Original: return translate('preferences.original'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts b/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts deleted file mode 100644 index cc6e01449..000000000 --- a/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {ScrobbleProvider} from "../_services/scrobbling.service"; - -@Pipe({ - name: 'scrobbleProviderName', - standalone: true -}) -export class ScrobbleProviderNamePipe implements PipeTransform { - - transform(value: ScrobbleProvider): string { - switch (value) { - case ScrobbleProvider.AniList: return 'AniList'; - case ScrobbleProvider.Mal: return 'MAL'; - case ScrobbleProvider.Kavita: return 'Kavita'; - case ScrobbleProvider.Cbr: return 'Comicbook Roundup'; - case ScrobbleProvider.GoogleBooks: return 'Google Books'; - } - } - -} diff --git a/UI/Web/src/app/_pipes/setting-fragment.pipe.ts b/UI/Web/src/app/_pipes/setting-fragment.pipe.ts deleted file mode 100644 index 14425d21c..000000000 --- a/UI/Web/src/app/_pipes/setting-fragment.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component"; -import {translate} from "@jsverse/transloco"; - -/** - * Translates the fragment for Settings to a User title - */ -@Pipe({ - name: 'settingFragment', - standalone: true -}) -export class SettingFragmentPipe implements PipeTransform { - - transform(tabID: SettingsTabId | string): string { - return translate('settings.' + tabID); - } -} diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts deleted file mode 100644 index d032de9c8..000000000 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {SortField} from "../_models/metadata/series-filter"; -import {TranslocoService} from "@jsverse/transloco"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; - -@Pipe({ - name: 'sortField', - standalone: true -}) -export class SortFieldPipe implements PipeTransform { - - constructor(private translocoService: TranslocoService) { - } - - transform(value: T, entityType: ValidFilterEntity): string { - - switch (entityType) { - case 'series': - return this.seriesSortFields(value as SortField); - case 'person': - return this.personSortFields(value as PersonSortField); - - } - } - - private personSortFields(value: PersonSortField) { - switch (value) { - case PersonSortField.Name: - return this.translocoService.translate('sort-field-pipe.person-name'); - case PersonSortField.SeriesCount: - return this.translocoService.translate('sort-field-pipe.person-series-count'); - case PersonSortField.ChapterCount: - return this.translocoService.translate('sort-field-pipe.person-chapter-count'); - - } - } - - private seriesSortFields(value: SortField) { - switch (value) { - case SortField.SortName: - return this.translocoService.translate('sort-field-pipe.sort-name'); - case SortField.Created: - return this.translocoService.translate('sort-field-pipe.created'); - case SortField.LastModified: - return this.translocoService.translate('sort-field-pipe.last-modified'); - case SortField.LastChapterAdded: - return this.translocoService.translate('sort-field-pipe.last-chapter-added'); - case SortField.TimeToRead: - return this.translocoService.translate('sort-field-pipe.time-to-read'); - case SortField.ReleaseYear: - return this.translocoService.translate('sort-field-pipe.release-year'); - case SortField.ReadProgress: - return this.translocoService.translate('sort-field-pipe.read-progress'); - case SortField.AverageRating: - return this.translocoService.translate('sort-field-pipe.average-rating'); - case SortField.Random: - return this.translocoService.translate('sort-field-pipe.random'); - } - } - -} diff --git a/UI/Web/src/app/_pipes/utc-to-locale-date.pipe.ts b/UI/Web/src/app/_pipes/utc-to-locale-date.pipe.ts deleted file mode 100644 index 0a25eefdc..000000000 --- a/UI/Web/src/app/_pipes/utc-to-locale-date.pipe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {DateTime} from "luxon"; - -@Pipe({ - name: 'utcToLocaleDate', - standalone: true -}) -/** - * This is the same as the UtcToLocalTimePipe but returning a timezone aware DateTime object rather than a string. - * Use this when the next operation needs a Date object (like the TimeAgoPipe) - */ -export class UtcToLocaleDatePipe implements PipeTransform { - - transform(utcDate: string | undefined | null): Date | null { - if (utcDate === '' || utcDate === null || utcDate === undefined || utcDate.split('T')[0] === '0001-01-01') { - return null; - } - - const browserLanguage = navigator.language; - const dateTime = DateTime.fromISO(utcDate, { zone: 'utc' }).toLocal().setLocale(browserLanguage); - return dateTime.toJSDate() - } - -} diff --git a/UI/Web/src/app/_pipes/writing-style.pipe.ts b/UI/Web/src/app/_pipes/writing-style.pipe.ts deleted file mode 100644 index 140978950..000000000 --- a/UI/Web/src/app/_pipes/writing-style.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {WritingStyle} from "../_models/preferences/writing-style"; -import {ScalingOption} from "../_models/preferences/scaling-option"; - -@Pipe({ - name: 'writingStyle', - standalone: true -}) -export class WritingStylePipe implements PipeTransform { - - transform(value: WritingStyle): string { - const v = parseInt(value + '', 10) as WritingStyle; - switch (v) { - case WritingStyle.Horizontal: return translate('preferences.horizontal'); - case WritingStyle.Vertical: return translate('preferences.vertical'); - } - } - -} diff --git a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts b/UI/Web/src/app/_resolvers/reading-profile.resolver.ts deleted file mode 100644 index 1d28adf95..000000000 --- a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; -import {Observable} from 'rxjs'; -import {ReadingProfileService} from "../_services/reading-profile.service"; - -@Injectable({ - providedIn: 'root' -}) -export class ReadingProfileResolver implements Resolve { - - constructor(private readingProfileService: ReadingProfileService) {} - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - // Extract seriesId from route params or parent route - const seriesId = route.params['seriesId'] || route.parent?.params['seriesId']; - return this.readingProfileService.getForSeries(seriesId); - } -} diff --git a/UI/Web/src/app/_resolvers/url-filter.resolver.ts b/UI/Web/src/app/_resolvers/url-filter.resolver.ts deleted file mode 100644 index 16bc5c752..000000000 --- a/UI/Web/src/app/_resolvers/url-filter.resolver.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Injectable} from "@angular/core"; -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; -import {Observable, of} from "rxjs"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; -import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; - -/** - * Checks the url for a filter and resolves one if applicable, otherwise returns null. - * It is up to the consumer to cast appropriately. - */ -@Injectable({ - providedIn: 'root' -}) -export class UrlFilterResolver implements Resolve { - - constructor(private filterUtilitiesService: FilterUtilitiesService) {} - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - if (!state.url.includes('?')) return of(null); - return this.filterUtilitiesService.decodeFilter(state.url.split('?')[1]); - } -} diff --git a/UI/Web/src/app/_routes/all-filters-routing.module.ts b/UI/Web/src/app/_routes/all-filters-routing.module.ts deleted file mode 100644 index d9794d271..000000000 --- a/UI/Web/src/app/_routes/all-filters-routing.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Routes} from "@angular/router"; -import {AllFiltersComponent} from "../all-filters/all-filters.component"; - - -export const routes: Routes = [ - {path: '', component: AllFiltersComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/all-series-routing.module.ts b/UI/Web/src/app/_routes/all-series-routing.module.ts deleted file mode 100644 index 5c4804251..000000000 --- a/UI/Web/src/app/_routes/all-series-routing.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Routes} from "@angular/router"; -import {AllSeriesComponent} from "../all-series/_components/all-series/all-series.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - - -export const routes: Routes = [ - {path: '', component: AllSeriesComponent, pathMatch: 'full', - runGuardsAndResolvers: 'always', - resolve: { - filter: UrlFilterResolver - } - }, -]; diff --git a/UI/Web/src/app/_routes/announcements-routing.module.ts b/UI/Web/src/app/_routes/announcements-routing.module.ts deleted file mode 100644 index 34c31b11a..000000000 --- a/UI/Web/src/app/_routes/announcements-routing.module.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Routes } from "@angular/router"; -import { AnnouncementsComponent } from "../announcements/_components/announcements/announcements.component"; - -export const routes: Routes = [ - {path: '', component: AnnouncementsComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/book-reader.router.module.ts b/UI/Web/src/app/_routes/book-reader.router.module.ts deleted file mode 100644 index c9d6262ad..000000000 --- a/UI/Web/src/app/_routes/book-reader.router.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {Routes} from '@angular/router'; -import {BookReaderComponent} from '../book-reader/_components/book-reader/book-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; - -export const routes: Routes = [ - { - path: ':chapterId', - component: BookReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } - } -]; - diff --git a/UI/Web/src/app/_routes/bookmark-routing.module.ts b/UI/Web/src/app/_routes/bookmark-routing.module.ts deleted file mode 100644 index 2c7c52036..000000000 --- a/UI/Web/src/app/_routes/bookmark-routing.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {Routes} from "@angular/router"; -import {BookmarksComponent} from "../bookmark/_components/bookmarks/bookmarks.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - -export const routes: Routes = [ - {path: '', component: BookmarksComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, -]; diff --git a/UI/Web/src/app/_routes/browse-routing.module.ts b/UI/Web/src/app/_routes/browse-routing.module.ts deleted file mode 100644 index be96e8193..000000000 --- a/UI/Web/src/app/_routes/browse-routing.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Routes} from "@angular/router"; -import {BrowsePeopleComponent} from "../browse/browse-people/browse-people.component"; -import {BrowseGenresComponent} from "../browse/browse-genres/browse-genres.component"; -import {BrowseTagsComponent} from "../browse/browse-tags/browse-tags.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - - -export const routes: Routes = [ - // Legacy route - {path: 'authors', component: BrowsePeopleComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, - {path: 'people', component: BrowsePeopleComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, - {path: 'genres', component: BrowseGenresComponent, pathMatch: 'full'}, - {path: 'tags', component: BrowseTagsComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/collections-routing.module.ts b/UI/Web/src/app/_routes/collections-routing.module.ts deleted file mode 100644 index 2b3b0ffd7..000000000 --- a/UI/Web/src/app/_routes/collections-routing.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Routes} from '@angular/router'; -import {AllCollectionsComponent} from '../collections/_components/all-collections/all-collections.component'; -import {CollectionDetailComponent} from '../collections/_components/collection-detail/collection-detail.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - -export const routes: Routes = [ - {path: '', component: AllCollectionsComponent, pathMatch: 'full'}, - {path: ':id', component: CollectionDetailComponent, - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, -]; - diff --git a/UI/Web/src/app/_routes/dashboard-routing.module.ts b/UI/Web/src/app/_routes/dashboard-routing.module.ts deleted file mode 100644 index e035a47ea..000000000 --- a/UI/Web/src/app/_routes/dashboard-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Routes } from '@angular/router'; -import { DashboardComponent } from '../dashboard/_components/dashboard.component'; - - -export const routes: Routes = [ - { - path: '', - component: DashboardComponent, - } -]; diff --git a/UI/Web/src/app/_routes/library-detail-routing.module.ts b/UI/Web/src/app/_routes/library-detail-routing.module.ts deleted file mode 100644 index 3c09a71ee..000000000 --- a/UI/Web/src/app/_routes/library-detail-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Routes} from '@angular/router'; -import {AuthGuard} from '../_guards/auth.guard'; -import {LibraryAccessGuard} from '../_guards/library-access.guard'; -import {LibraryDetailComponent} from '../library-detail/library-detail.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - - -export const routes: Routes = [ - { - path: ':libraryId', - runGuardsAndResolvers: 'always', - canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent, - resolve: { - filter: UrlFilterResolver - }, - }, - { - path: '', - runGuardsAndResolvers: 'always', - canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent, - resolve: { - filter: UrlFilterResolver - }, - }, -]; diff --git a/UI/Web/src/app/_routes/manga-reader.router.module.ts b/UI/Web/src/app/_routes/manga-reader.router.module.ts deleted file mode 100644 index e479e8ae6..000000000 --- a/UI/Web/src/app/_routes/manga-reader.router.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Routes} from '@angular/router'; -import {MangaReaderComponent} from '../manga-reader/_components/manga-reader/manga-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; - -export const routes: Routes = [ - { - path: ':chapterId', - component: MangaReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } - }, - { - // This will allow the MangaReader to have a list to use for next/prev chapters rather than natural sort order - path: ':chapterId/list/:listId', - component: MangaReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } - } -]; - diff --git a/UI/Web/src/app/_routes/pdf-reader.router.module.ts b/UI/Web/src/app/_routes/pdf-reader.router.module.ts deleted file mode 100644 index 7cb9f68e2..000000000 --- a/UI/Web/src/app/_routes/pdf-reader.router.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Routes} from '@angular/router'; -import {PdfReaderComponent} from '../pdf-reader/_components/pdf-reader/pdf-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; - -export const routes: Routes = [ - { - path: ':chapterId', - component: PdfReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } - } -]; diff --git a/UI/Web/src/app/_routes/person-detail-routing.module.ts b/UI/Web/src/app/_routes/person-detail-routing.module.ts deleted file mode 100644 index 95b610cea..000000000 --- a/UI/Web/src/app/_routes/person-detail-routing.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Routes } from '@angular/router'; -import { AuthGuard } from '../_guards/auth.guard'; -import {PersonDetailComponent} from "../person-detail/person-detail.component"; - - -export const routes: Routes = [ - { - path: ':name', - runGuardsAndResolvers: 'always', - canActivate: [AuthGuard], - component: PersonDetailComponent - }, - { - path: '', - runGuardsAndResolvers: 'always', - canActivate: [AuthGuard], - component: PersonDetailComponent - } -]; diff --git a/UI/Web/src/app/_routes/reading-list-routing.module.ts b/UI/Web/src/app/_routes/reading-list-routing.module.ts deleted file mode 100644 index f1c8e1410..000000000 --- a/UI/Web/src/app/_routes/reading-list-routing.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from "@angular/router"; -import { ReadingListDetailComponent } from "../reading-list/_components/reading-list-detail/reading-list-detail.component"; -import { ReadingListsComponent } from "../reading-list/_components/reading-lists/reading-lists.component"; - - -export const routes: Routes = [ - {path: '', component: ReadingListsComponent, pathMatch: 'full'}, - {path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/registration.router.module.ts b/UI/Web/src/app/_routes/registration.router.module.ts deleted file mode 100644 index 266c43187..000000000 --- a/UI/Web/src/app/_routes/registration.router.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Routes } from '@angular/router'; -import { UserLoginComponent } from '../registration/user-login/user-login.component'; -import { ConfirmEmailChangeComponent } from '../registration/_components/confirm-email-change/confirm-email-change.component'; -import { ConfirmEmailComponent } from '../registration/_components/confirm-email/confirm-email.component'; -import { ConfirmMigrationEmailComponent } from '../registration/_components/confirm-migration-email/confirm-migration-email.component'; -import { ConfirmResetPasswordComponent } from '../registration/_components/confirm-reset-password/confirm-reset-password.component'; -import { RegisterComponent } from '../registration/_components/register/register.component'; -import { ResetPasswordComponent } from '../registration/_components/reset-password/reset-password.component'; - -export const routes: Routes = [ - { - path: '', - component: UserLoginComponent - }, - { - path: 'login', - component: UserLoginComponent - }, - { - path: 'confirm-email', - component: ConfirmEmailComponent, - }, - { - path: 'confirm-migration-email', - component: ConfirmMigrationEmailComponent, - }, - { - path: 'confirm-email-update', - component: ConfirmEmailChangeComponent, - }, - { - path: 'register', - component: RegisterComponent, - }, - { - path: 'reset-password', - component: ResetPasswordComponent - }, - { - path: 'confirm-reset-password', - component: ConfirmResetPasswordComponent - } -]; diff --git a/UI/Web/src/app/_routes/settings-routing.module.ts b/UI/Web/src/app/_routes/settings-routing.module.ts deleted file mode 100644 index 83cb040ef..000000000 --- a/UI/Web/src/app/_routes/settings-routing.module.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Routes } from '@angular/router'; -import {SettingsComponent} from "../settings/_components/settings/settings.component"; - -export const routes: Routes = [ - {path: '', component: SettingsComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/want-to-read-routing.module.ts b/UI/Web/src/app/_routes/want-to-read-routing.module.ts deleted file mode 100644 index b593172c0..000000000 --- a/UI/Web/src/app/_routes/want-to-read-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Routes} from '@angular/router'; -import {WantToReadComponent} from '../want-to-read/_components/want-to-read/want-to-read.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - -export const routes: Routes = [ - {path: '', component: WantToReadComponent, pathMatch: 'full', runGuardsAndResolvers: 'always', resolve: { - filter: UrlFilterResolver - } - }, -]; diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index f1f91143f..88dc2dc38 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -1,54 +1,35 @@ -import {HttpClient} from '@angular/common/http'; -import {DestroyRef, inject, Injectable} from '@angular/core'; -import {Observable, of, ReplaySubject, shareReplay} from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import {DestroyRef, inject, Injectable } from '@angular/core'; +import {catchError, of, ReplaySubject, throwError} from 'rxjs'; import {filter, map, switchMap, tap} from 'rxjs/operators'; -import {environment} from 'src/environments/environment'; -import {Preferences} from '../_models/preferences/preferences'; -import {User} from '../_models/user'; -import {Router} from '@angular/router'; -import {EVENTS, MessageHubService} from './message-hub.service'; -import {ThemeService} from './theme.service'; -import {InviteUserResponse} from '../_models/auth/invite-user-response'; -import {UserUpdateEvent} from '../_models/events/user-update-event'; -import {AgeRating} from '../_models/metadata/age-rating'; -import {AgeRestriction} from '../_models/metadata/age-restriction'; -import {TextResonse} from '../_types/text-response'; +import { environment } from 'src/environments/environment'; +import { Preferences } from '../_models/preferences/preferences'; +import { User } from '../_models/user'; +import { Router } from '@angular/router'; +import { EVENTS, MessageHubService } from './message-hub.service'; +import { ThemeService } from './theme.service'; +import { InviteUserResponse } from '../_models/auth/invite-user-response'; +import { UserUpdateEvent } from '../_models/events/user-update-event'; +import { UpdateEmailResponse } from '../_models/auth/update-email-response'; +import { AgeRating } from '../_models/metadata/age-rating'; +import { AgeRestriction } from '../_models/metadata/age-restriction'; +import { TextResonse } from '../_types/text-response'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {Action} from "./action-factory.service"; -import {LicenseService} from "./license.service"; -import {LocalizationService} from "./localization.service"; export enum Role { Admin = 'Admin', ChangePassword = 'Change Password', Bookmark = 'Bookmark', Download = 'Download', - ChangeRestriction = 'Change Restriction', - ReadOnly = 'Read Only', - Login = 'Login', - Promote = 'Promote', + ChangeRestriction = 'Change Restriction' } -export const allRoles = [ - Role.Admin, - Role.ChangePassword, - Role.Bookmark, - Role.Download, - Role.ChangeRestriction, - Role.ReadOnly, - Role.Login, - Role.Promote, -] - @Injectable({ providedIn: 'root' }) export class AccountService { private readonly destroyRef = inject(DestroyRef); - private readonly licenseService = inject(LicenseService); - private readonly localizationService = inject(LocalizationService); - baseUrl = environment.apiUrl; userKey = 'kavita-user'; public static lastLoginKey = 'kavita-lastlogin'; @@ -57,21 +38,19 @@ export class AccountService { // Stores values, when someone subscribes gives (1) of last values seen. private currentUserSource = new ReplaySubject(1); - public currentUser$ = this.currentUserSource.asObservable().pipe(takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true})); - public isAdmin$: Observable = this.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(u => { - if (!u) return false; - return this.hasAdminRole(u); - }), shareReplay({bufferSize: 1, refCount: true})); - + public currentUser$ = this.currentUserSource.asObservable(); + private hasValidLicenseSource = new ReplaySubject(1); + /** + * Does the user have an active license + */ + public hasValidLicense$ = this.hasValidLicenseSource.asObservable(); /** * SetTimeout handler for keeping track of refresh token call */ private refreshTokenTimeout: ReturnType | undefined; - private isOnline: boolean = true; - constructor(private httpClient: HttpClient, private router: Router, private messageHub: MessageHubService, private themeService: ThemeService) { messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate), @@ -79,84 +58,6 @@ export class AccountService { filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username), switchMap(() => this.refreshAccount())) .subscribe(() => {}); - - window.addEventListener("offline", (e) => { - this.isOnline = false; - }); - - window.addEventListener("online", (e) => { - this.isOnline = true; - this.refreshToken().subscribe(); - }); - } - - canInvokeAction(user: User, action: Action) { - const isAdmin = this.hasAdminRole(user); - const canDownload = this.hasDownloadRole(user); - const canPromote = this.hasPromoteRole(user); - - if (isAdmin) return true; - if (action === Action.Download) return canDownload; - if (action === Action.Promote || action === Action.UnPromote) return canPromote; - if (action === Action.Delete) return isAdmin; - return true; - } - - /** - * If the user has any role in the restricted roles array or is an Admin - * @param user - * @param roles - * @param restrictedRoles - */ - hasAnyRole(user: User, roles: Array, restrictedRoles: Array = []) { - if (!user || !user.roles) { - return false; - } - - // If the user is an admin, they have the role - if (this.hasAdminRole(user)) { - return true; - } - - // If restricted roles are provided and the user has any of them, deny access - if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) { - return false; - } - - // If roles are empty, allow access (no restrictions by roles) - if (roles.length === 0) { - return true; - } - - // Allow access if the user has any of the allowed roles - return roles.some(role => user.roles.includes(role)); - } - - /** - * If User or Admin, will return false - * @param user - * @param restrictedRoles - */ - hasAnyRestrictedRole(user: User, restrictedRoles: Array = []) { - if (!user || !user.roles) { - return true; - } - - if (restrictedRoles.length === 0) { - return false; - } - - // If the user is an admin, they have the role - if (this.hasAdminRole(user)) { - return false; - } - - - if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) { - return true; - } - - return false; } hasAdminRole(user: User) { @@ -168,7 +69,7 @@ export class AccountService { } hasChangeAgeRestrictionRole(user: User) { - return user && !user.roles.includes(Role.Admin) && user.roles.includes(Role.ChangeRestriction); + return user && user.roles.includes(Role.ChangeRestriction); } hasDownloadRole(user: User) { @@ -179,23 +80,43 @@ export class AccountService { return user && user.roles.includes(Role.Bookmark); } - hasReadOnlyRole(user: User) { - return user && user.roles.includes(Role.ReadOnly); - } - - hasPromoteRole(user: User) { - return user && user.roles.includes(Role.Promote) || user.roles.includes(Role.Admin); - } - getRoles() { return this.httpClient.get(this.baseUrl + 'account/roles'); } + deleteLicense() { + return this.httpClient.delete(this.baseUrl + 'license', TextResonse); + } + hasValidLicense(forceCheck: boolean = false) { + return this.httpClient.get(this.baseUrl + 'license/valid-license?forceCheck=' + forceCheck, TextResonse) + .pipe( + map(res => res === "true"), + tap(res => { + this.hasValidLicenseSource.next(res) + }), + catchError(error => { + this.hasValidLicenseSource.next(false); + return throwError(error); // Rethrow the error to propagate it further + }) + ); + } + + hasAnyLicense() { + return this.httpClient.get(this.baseUrl + 'license/has-license', TextResonse) + .pipe( + map(res => res === "true"), + ); + } + + updateUserLicense(license: string, email: string) { + return this.httpClient.post(this.baseUrl + 'license', {license, email}, TextResonse) + .pipe(map(res => res === "true")); + } login(model: {username: string, password: string, apiKey?: string}) { return this.httpClient.post(this.baseUrl + 'account/login', model).pipe( - tap((response: User) => { + map((response: User) => { const user = response; if (user) { this.setCurrentUser(user); @@ -205,9 +126,7 @@ export class AccountService { ); } - setCurrentUser(user?: User, refreshConnections = true) { - - const isSameUser = this.currentUser === user; + setCurrentUser(user?: User) { if (user) { user.roles = []; const roles = this.getDecodedToken(user.token).role; @@ -215,7 +134,6 @@ export class AccountService { localStorage.setItem(this.userKey, JSON.stringify(user)); localStorage.setItem(AccountService.lastLoginKey, user.username); - if (user.preferences && user.preferences.theme) { this.themeService.setTheme(user.preferences.theme.name); } else { @@ -228,18 +146,12 @@ export class AccountService { this.currentUser = user; this.currentUserSource.next(user); - if (!refreshConnections) return; - this.stopRefreshTokenTimer(); if (this.currentUser) { - // BUG: StopHubConnection has a promise in it, this needs to be async - // But that really messes everything up - if (!isSameUser) { - this.messageHub.stopHubConnection(); - this.messageHub.createHubConnection(this.currentUser); - this.licenseService.hasValidLicense().subscribe(); - } + this.messageHub.stopHubConnection(); + this.messageHub.createHubConnection(this.currentUser); + this.hasValidLicense().subscribe(); this.startRefreshTokenTimer(); } } @@ -273,9 +185,8 @@ export class AccountService { return this.httpClient.get(this.baseUrl + 'account/email-confirmed'); } - isEmailValid() { - return this.httpClient.get(this.baseUrl + 'account/is-email-valid', TextResonse) - .pipe(map(res => res == "true")); + migrateUser(model: {email: string, username: string, password: string, sendEmail: boolean}) { + return this.httpClient.post(this.baseUrl + 'account/migrate-email', model, TextResonse); } confirmMigrationEmail(model: {email: string, token: string}) { @@ -283,7 +194,7 @@ export class AccountService { } resendConfirmationEmail(userId: number) { - return this.httpClient.post(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}); + return this.httpClient.post(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, TextResonse); } inviteUser(model: {email: string, roles: Array, libraries: Array, ageRestriction: AgeRestriction}) { @@ -329,7 +240,7 @@ export class AccountService { } updateEmail(email: string, password: string) { - return this.httpClient.post(this.baseUrl + 'account/update/email', {email, password}); + return this.httpClient.post(this.baseUrl + 'account/update/email', {email, password}); } updateAgeRestriction(ageRating: AgeRating, includeUnknowns: boolean) { @@ -354,12 +265,10 @@ export class AccountService { return this.httpClient.post(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => { if (this.currentUser !== undefined && this.currentUser !== null) { this.currentUser.preferences = settings; - this.setCurrentUser(this.currentUser, false); + this.setCurrentUser(this.currentUser); // Update the locale on disk (for logout and compact-number pipe) localStorage.setItem(AccountService.localeKey, this.currentUser.preferences.locale); - this.localizationService.refreshTranslations(this.currentUser.preferences.locale); - } return settings; }), takeUntilDestroyed(this.destroyRef)); @@ -396,7 +305,7 @@ export class AccountService { } - refreshAccount() { + private refreshAccount() { if (this.currentUser === null || this.currentUser === undefined) return of(); return this.httpClient.get(this.baseUrl + 'account/refresh-account').pipe(map((user: User) => { if (user) { @@ -410,7 +319,7 @@ export class AccountService { private refreshToken() { - if (this.currentUser === null || this.currentUser === undefined || !this.isOnline) return of(); + if (this.currentUser === null || this.currentUser === undefined) return of(); return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token', {token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => { if (this.currentUser) { diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index e5967bf24..75229bd47 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -1,19 +1,15 @@ -import {Injectable} from '@angular/core'; -import {map, Observable, shareReplay} from 'rxjs'; -import {Chapter} from '../_models/chapter'; -import {UserCollection} from '../_models/collection-tag'; -import {Device} from '../_models/device/device'; -import {Library} from '../_models/library/library'; -import {ReadingList} from '../_models/reading-list'; -import {Series} from '../_models/series'; -import {Volume} from '../_models/volume'; -import {AccountService, Role} from './account.service'; -import {DeviceService} from './device.service'; +import { Injectable } from '@angular/core'; +import { map, Observable, shareReplay } from 'rxjs'; +import { Chapter } from '../_models/chapter'; +import { CollectionTag } from '../_models/collection-tag'; +import { Device } from '../_models/device/device'; +import { Library } from '../_models/library'; +import { ReadingList } from '../_models/reading-list'; +import { Series } from '../_models/series'; +import { Volume } from '../_models/volume'; +import { AccountService } from './account.service'; +import { DeviceService } from './device.service'; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; -import {SmartFilter} from "../_models/metadata/v2/smart-filter"; -import {translate} from "@jsverse/transloco"; -import {Person} from "../_models/metadata/person"; -import {User} from '../_models/user'; export enum Action { Submenu = -1, @@ -101,55 +97,12 @@ export enum Action { RemoveRuleGroup = 21, MarkAsVisible = 22, MarkAsInvisible = 23, - /** - * Promotes the underlying item (Reading List, Collection) - */ - Promote = 24, - UnPromote = 25, - /** - * Invoke refresh covers as false to generate colorscapes - */ - GenerateColorScape = 26, - /** - * Copy settings from one entity to another - */ - CopySettings = 27, - /** - * Match an entity with an upstream system - */ - Match = 28, - /** - * Merge two (or more?) entities - */ - Merge = 29, - /** - * Add to a reading profile - */ - SetReadingProfile = 30, - /** - * Remove the reading profile from the entity - */ - ClearReadingProfile = 31, } -/** - * Callback for an action - */ -export type ActionCallback = (action: ActionItem, entity: T) => void; -export type ActionShouldRenderFunc = (action: ActionItem, entity: T, user: User) => boolean; - export interface ActionItem { title: string; - description: string; action: Action; - callback: ActionCallback; - /** - * Roles required to be present for ActionItem to show. If empty, assumes anyone can see. At least one needs to apply. - */ - requiredRoles: Role[]; - /** - * @deprecated Use required Roles instead - */ + callback: (action: ActionItem, data: T) => void; requiresAdmin: boolean; children: Array>; /** @@ -165,98 +118,87 @@ export interface ActionItem { * Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return */ _extra?: {title: string, data: any}; - /** - * Will call on each action to determine if it should show for the appropriate entity based on state and user - */ - shouldRender: ActionShouldRenderFunc; } -/** - * Entities that can be actioned upon - */ -export type ActionableEntity = Volume | Series | Chapter | ReadingList | UserCollection | Person | Library | SideNavStream | SmartFilter | null; - @Injectable({ providedIn: 'root', }) export class ActionFactoryService { - private libraryActions: Array> = []; - private seriesActions: Array> = []; - private volumeActions: Array> = []; - private chapterActions: Array> = []; - private collectionTagActions: Array> = []; - private readingListActions: Array> = []; - private bookmarkActions: Array> = []; - private personActions: Array> = []; - private sideNavStreamActions: Array> = []; - private smartFilterActions: Array> = []; - private sideNavHomeActions: Array> = []; + libraryActions: Array> = []; + + seriesActions: Array> = []; + + volumeActions: Array> = []; + + chapterActions: Array> = []; + + collectionTagActions: Array> = []; + + readingListActions: Array> = []; + + bookmarkActions: Array> = []; + + sideNavStreamActions: Array> = []; + + isAdmin = false; + hasDownloadRole = false; constructor(private accountService: AccountService, private deviceService: DeviceService) { - this.accountService.currentUser$.subscribe((_) => { + this.accountService.currentUser$.subscribe((user) => { + if (user) { + this.isAdmin = this.accountService.hasAdminRole(user); + this.hasDownloadRole = this.accountService.hasDownloadRole(user); + } else { + this._resetActions(); + return; // If user is logged out, we don't need to do anything + } + this._resetActions(); }); } - getLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.libraryActions, callback, shouldRenderFunc) as ActionItem[]; + getLibraryActions(callback: (action: ActionItem, library: Library) => void) { + return this.applyCallbackToList(this.libraryActions, callback); } - getSeriesActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.seriesActions, callback, shouldRenderFunc); + getSeriesActions(callback: (action: ActionItem, series: Series) => void) { + return this.applyCallbackToList(this.seriesActions, callback); } - getSideNavStreamActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.sideNavStreamActions, callback, shouldRenderFunc); + getSideNavStreamActions(callback: (action: ActionItem, series: SideNavStream) => void) { + return this.applyCallbackToList(this.sideNavStreamActions, callback); } - getSmartFilterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.smartFilterActions, callback, shouldRenderFunc); + getVolumeActions(callback: (action: ActionItem, volume: Volume) => void) { + return this.applyCallbackToList(this.volumeActions, callback); } - getVolumeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.volumeActions, callback, shouldRenderFunc); + getChapterActions(callback: (action: ActionItem, chapter: Chapter) => void) { + return this.applyCallbackToList(this.chapterActions, callback); } - getChapterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.chapterActions, callback, shouldRenderFunc); + getCollectionTagActions(callback: (action: ActionItem, collectionTag: CollectionTag) => void) { + return this.applyCallbackToList(this.collectionTagActions, callback); } - getCollectionTagActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.collectionTagActions, callback, shouldRenderFunc); + getReadingListActions(callback: (action: ActionItem, readingList: ReadingList) => void) { + return this.applyCallbackToList(this.readingListActions, callback); } - getReadingListActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.readingListActions, callback, shouldRenderFunc); + getBookmarkActions(callback: (action: ActionItem, series: Series) => void) { + return this.applyCallbackToList(this.bookmarkActions, callback); } - getBookmarkActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.bookmarkActions, callback, shouldRenderFunc); + getMetadataFilterActions(callback: (action: ActionItem, data: any) => void) { + const actions = [ + {title: 'add-rule-group-and', action: Action.AddRuleGroup, requiresAdmin: false, children: [], callback: this.dummyCallback}, + {title: 'add-rule-group-or', action: Action.AddRuleGroup, requiresAdmin: false, children: [], callback: this.dummyCallback}, + {title: 'remove-rule-group', action: Action.RemoveRuleGroup, requiresAdmin: false, children: [], callback: this.dummyCallback}, + ]; + return this.applyCallbackToList(actions, callback); } - getPersonActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.personActions, callback, shouldRenderFunc); - } - - getSideNavHomeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.sideNavHomeActions, callback, shouldRenderFunc); - } - - dummyCallback(action: ActionItem, entity: any) {} - dummyShouldRender(action: ActionItem, entity: any, user: User) {return true;} - basicReadRender(action: ActionItem, entity: any, user: User) { - if (entity === null || entity === undefined) return true; - if (!entity.hasOwnProperty('pagesRead') && !entity.hasOwnProperty('pages')) return true; - - switch (action.action) { - case(Action.MarkAsRead): - return entity.pagesRead < entity.pages; - case(Action.MarkAsUnread): - return entity.pagesRead !== 0; - default: - return true; - } - } + dummyCallback(action: ActionItem, data: any) {} filterSendToAction(actions: Array>, chapter: Chapter) { // if (chapter.files.filter(f => f.format === MangaFormat.EPUB || f.format === MangaFormat.PDF).length !== chapter.files.length) { @@ -266,168 +208,33 @@ export class ActionFactoryService { return actions; } - getActionablesForSettingsPage(actions: Array>, blacklist: Array = []) { - const tasks = []; - - let actionItem; - for (let parent of actions) { - if (parent.action === Action.SendTo) continue; - - if (parent.children.length === 0) { - actionItem = {...parent}; - actionItem.title = translate('actionable.' + actionItem.title); - if (actionItem.description !== '') { - actionItem.description = translate('actionable.' + actionItem.description); - } - - tasks.push(actionItem); - continue; - } - - for (let child of parent.children) { - if (child.action === Action.SendTo) continue; - actionItem = {...child}; - actionItem.title = translate('actionable.' + actionItem.title); - if (actionItem.description !== '') { - actionItem.description = translate('actionable.' + actionItem.description); - } - tasks.push(actionItem); - } - } - - // Filter out tasks that don't make sense - return tasks.filter(t => !blacklist.includes(t.action)); - } - - getBulkLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - - // Scan is currently not supported due to the backend not being able to handle it yet - const actions = this.flattenActions(this.libraryActions).filter(a => { - return [Action.Delete, Action.GenerateColorScape, Action.AnalyzeFiles, Action.RefreshMetadata, Action.CopySettings].includes(a.action); - }); - - actions.push({ - _extra: undefined, - class: undefined, - description: '', - dynamicList: undefined, - action: Action.CopySettings, - callback: this.dummyCallback, - shouldRender: shouldRenderFunc, - children: [], - requiredRoles: [Role.Admin], - requiresAdmin: true, - title: 'copy-settings' - }) - return this.applyCallbackToList(actions, callback, shouldRenderFunc) as ActionItem[]; - } - - flattenActions(actions: Array>): Array> { - return actions.reduce>>((flatArray, action) => { - if (action.action !== Action.Submenu) { - flatArray.push(action); - } - - // Recursively flatten the children, if any - if (action.children && action.children.length > 0) { - flatArray.push(...this.flattenActions(action.children)); - } - - return flatArray; - }, [] as Array>); // Explicitly defining the type of flatArray - } - - private _resetActions() { this.libraryActions = [ { action: Action.Scan, title: 'scan-library', - description: 'scan-library-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, - { - action: Action.Submenu, - title: 'reading-profiles', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.SetReadingProfile, - title: 'set-reading-profile', - description: 'set-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.ClearReadingProfile, - title: 'clear-reading-profile', - description: 'clear-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ], - }, { action: Action.Submenu, title: 'others', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', - description: 'refresh-covers-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.GenerateColorScape, - title: 'generate-colorscape', - description: 'generate-colorscape-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { action: Action.AnalyzeFiles, title: 'analyze-files', - description: 'analyze-files-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.Delete, - title: 'delete', - description: 'delete-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ], @@ -435,11 +242,8 @@ export class ActionFactoryService { { action: Action.Edit, title: 'settings', - description: 'settings-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ]; @@ -448,145 +252,89 @@ export class ActionFactoryService { { action: Action.Edit, title: 'edit', - description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], + requiresAdmin: true, children: [], }, { action: Action.Delete, title: 'delete', - description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], class: 'danger', children: [], }, - { - action: Action.Promote, - title: 'promote', - description: 'promote-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.UnPromote, - title: 'unpromote', - description: 'unpromote-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, ]; this.seriesActions = [ { action: Action.MarkAsRead, title: 'mark-as-read', - description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsUnread, title: 'mark-as-unread', - description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.Scan, title: 'scan-series', - description: 'scan-series-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { action: Action.Submenu, title: 'add-to', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ - { + { action: Action.AddToWantToReadList, title: 'add-to-want-to-read', - description: 'add-to-want-to-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.RemoveFromWantToReadList, title: 'remove-from-want-to-read', - description: 'remove-to-want-to-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.AddToReadingList, title: 'add-to-reading-list', - description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.AddToCollection, title: 'add-to-collection', - description: 'add-to-collection-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], + requiresAdmin: true, children: [], - } + }, ], }, { action: Action.Submenu, title: 'send-to', - description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -594,117 +342,48 @@ export class ActionFactoryService { } ], }, - { - action: Action.Submenu, - title: 'reading-profiles', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.SetReadingProfile, - title: 'set-reading-profile', - description: 'set-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.ClearReadingProfile, - title: 'clear-reading-profile', - description: 'clear-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ], - }, { action: Action.Submenu, title: 'others', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', - description: 'refresh-covers-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.GenerateColorScape, - title: 'generate-colorscape', - description: 'generate-colorscape-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { action: Action.AnalyzeFiles, title: 'analyze-files', - description: 'analyze-files-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { action: Action.Delete, title: 'delete', - description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], class: 'danger', children: [], }, ], }, - { - action: Action.Match, - title: 'match', - description: 'match-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, { action: Action.Download, title: 'download', - description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [Role.Download], children: [], }, { action: Action.Edit, title: 'edit', - description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ]; @@ -713,71 +392,50 @@ export class ActionFactoryService { { action: Action.IncognitoRead, title: 'read-incognito', - description: 'read-incognito-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsRead, title: 'mark-as-read', - description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsUnread, title: 'mark-as-unread', - description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, - { - action: Action.Submenu, - title: 'add-to', - description: '=', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.AddToReadingList, - title: 'add-to-reading-list', - description: 'add-to-reading-list-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - } - ] - }, + { + action: Action.Submenu, + title: 'add-to', + callback: this.dummyCallback, + requiresAdmin: false, + children: [ + { + action: Action.AddToReadingList, + title: 'add-to-reading-list', + callback: this.dummyCallback, + requiresAdmin: false, + children: [], + } + ] + }, { action: Action.Submenu, title: 'send-to', - description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -786,44 +444,17 @@ export class ActionFactoryService { ], }, { - action: Action.Submenu, - title: 'others', - description: '', + action: Action.Download, + title: 'download', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.Delete, - title: 'delete', - description: 'delete-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.Download, - title: 'download', - description: 'download-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ] + children: [], }, { action: Action.Edit, title: 'details', - description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -832,71 +463,50 @@ export class ActionFactoryService { { action: Action.IncognitoRead, title: 'read-incognito', - description: 'read-incognito-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsRead, title: 'mark-as-read', - description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsUnread, title: 'mark-as-unread', - description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, - { - action: Action.Submenu, - title: 'add-to', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.AddToReadingList, - title: 'add-to-reading-list', - description: 'add-to-reading-list-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - } - ] - }, + { + action: Action.Submenu, + title: 'add-to', + callback: this.dummyCallback, + requiresAdmin: false, + children: [ + { + action: Action.AddToReadingList, + title: 'add-to-reading-list', + callback: this.dummyCallback, + requiresAdmin: false, + children: [], + } + ] + }, { action: Action.Submenu, title: 'send-to', - description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', - description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -904,46 +514,19 @@ export class ActionFactoryService { } ], }, - // RBS will handle rendering this, so non-admins with download are applicable + // RBS will handle rendering this, so non-admins with download are appicable { - action: Action.Submenu, - title: 'others', - description: '', + action: Action.Download, + title: 'download', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.Delete, - title: 'delete', - description: 'delete-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.Download, - title: 'download', - description: 'download-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [Role.Download], - children: [], - }, - ] + children: [], }, { action: Action.Edit, - title: 'edit', - description: 'edit-tooltip', + title: 'details', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -952,99 +535,41 @@ export class ActionFactoryService { { action: Action.Edit, title: 'edit', - description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.Delete, title: 'delete', - description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], class: 'danger', children: [], }, - { - action: Action.Promote, - title: 'promote', - description: 'promote-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.UnPromote, - title: 'unpromote', - description: 'unpromote-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ]; - - this.personActions = [ - { - action: Action.Edit, - title: 'edit', - description: 'edit-person-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.Merge, - title: 'merge', - description: 'merge-person-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - } ]; this.bookmarkActions = [ { action: Action.ViewSeries, title: 'view-series', - description: 'view-series-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.DownloadBookmark, title: 'download', - description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.Delete, title: 'clear', - description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, class: 'danger', requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -1053,90 +578,37 @@ export class ActionFactoryService { { action: Action.MarkAsVisible, title: 'mark-visible', - description: 'mark-visible-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { action: Action.MarkAsInvisible, title: 'mark-invisible', - description: 'mark-invisible-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; - - this.smartFilterActions = [ - { - action: Action.Edit, - title: 'rename', - description: 'rename-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.Delete, - title: 'delete', - description: 'delete-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ]; - - this.sideNavHomeActions = [ - { - action: Action.Edit, - title: 'reorder', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - } - ] - - } - private applyCallback(action: ActionItem, callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc) { + private applyCallback(action: ActionItem, callback: (action: ActionItem, data: any) => void) { action.callback = callback; - action.shouldRender = shouldRenderFunc; if (action.children === null || action.children?.length === 0) return; - // Ensure action children are a copy of the parent (since parent does a shallow mapping) - action.children = action.children.map(d => { return {...d}; }); - - action.children.forEach((childAction) => { - this.applyCallback(childAction, callback, shouldRenderFunc); + action.children?.forEach((childAction) => { + this.applyCallback(childAction, callback); }); } - public applyCallbackToList(list: Array>, - callback: ActionCallback, - shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender): Array> { - // Create a clone of the list to ensure we aren't affecting the default state - const actions = list.map((a) => { - return { ...a }; - }); - - actions.forEach((action) => this.applyCallback(action, callback, shouldRenderFunc)); - - return actions; - } + public applyCallbackToList(list: Array>, callback: (action: ActionItem, data: any) => void): Array> { + const actions = list.map((a) => { + return { ...a }; + }); + actions.forEach((action) => this.applyCallback(action, callback)); + return actions; + } // Checks the whole tree for the action and returns true if it exists public hasAction(actions: Array>, action: Action) { diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 2328bf72e..1a6f4082b 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -1,40 +1,25 @@ -import {inject, Injectable} from '@angular/core'; -import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import {ToastrService} from 'ngx-toastr'; -import {take} from 'rxjs/operators'; -import {BulkAddToCollectionComponent} from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component'; -import {ADD_FLOW, AddToListModalComponent} from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; -import { - EditReadingListModalComponent -} from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; -import {ConfirmService} from '../shared/confirm.service'; -import { - LibrarySettingsModalComponent -} from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; -import {Chapter} from '../_models/chapter'; -import {Device} from '../_models/device/device'; -import {Library} from '../_models/library/library'; -import {ReadingList} from '../_models/reading-list'; -import {Series} from '../_models/series'; -import {Volume} from '../_models/volume'; -import {DeviceService} from './device.service'; -import {LibraryService} from './library.service'; -import {MemberService} from './member.service'; -import {ReaderService} from './reader.service'; -import {SeriesService} from './series.service'; -import {translate} from "@jsverse/transloco"; -import {UserCollection} from "../_models/collection-tag"; -import {CollectionTagService} from "./collection-tag.service"; -import {FilterService} from "./filter.service"; -import {ReadingListService} from "./reading-list.service"; -import {ChapterService} from "./chapter.service"; -import {VolumeService} from "./volume.service"; -import {DefaultModalOptions} from "../_models/default-modal-options"; -import {MatchSeriesModalComponent} from "../_single-module/match-series-modal/match-series-modal.component"; -import { - BulkSetReadingProfileModalComponent -} from "../cards/_modals/bulk-set-reading-profile-modal/bulk-set-reading-profile-modal.component"; - +import { Injectable, OnDestroy } from '@angular/core'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrService } from 'ngx-toastr'; +import { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component'; +import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; +import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; +import { ConfirmService } from '../shared/confirm.service'; +import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; +import { Chapter } from '../_models/chapter'; +import { Device } from '../_models/device/device'; +import { Library } from '../_models/library'; +import { ReadingList } from '../_models/reading-list'; +import { Series } from '../_models/series'; +import { Volume } from '../_models/volume'; +import { DeviceService } from './device.service'; +import { LibraryService } from './library.service'; +import { MemberService } from './member.service'; +import { ReaderService } from './reader.service'; +import { SeriesService } from './series.service'; +import {translate, TranslocoService} from "@ngneat/transloco"; export type LibraryActionCallback = (library: Partial) => void; export type SeriesActionCallback = (series: Series) => void; @@ -50,26 +35,20 @@ export type BooleanActionCallback = (result: boolean) => void; @Injectable({ providedIn: 'root' }) -export class ActionService { - - private readonly chapterService = inject(ChapterService); - private readonly volumeService = inject(VolumeService); - private readonly libraryService = inject(LibraryService); - private readonly seriesService = inject(SeriesService); - private readonly readerService = inject(ReaderService); - private readonly toastr = inject(ToastrService); - private readonly modalService = inject(NgbModal); - private readonly confirmService = inject(ConfirmService); - private readonly memberService = inject(MemberService); - private readonly deviceService = inject(DeviceService); - private readonly collectionTagService = inject(CollectionTagService); - private readonly filterService = inject(FilterService); - private readonly readingListService = inject(ReadingListService); - +export class ActionService implements OnDestroy { + private readonly onDestroy = new Subject(); private readingListModalRef: NgbModalRef | null = null; private collectionModalRef: NgbModalRef | null = null; + constructor(private libraryService: LibraryService, private seriesService: SeriesService, + private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal, + private confirmService: ConfirmService, private memberService: MemberService, private deviceService: DeviceService) { } + + ngOnDestroy() { + this.onDestroy.next(); + this.onDestroy.complete(); + } /** * Request a file scan for a given Library @@ -98,30 +77,24 @@ export class ActionService { * Request a refresh of Metadata for a given Library * @param library Partial Library, must have id and name populated * @param callback Optional callback to perform actions after API completes - * @param forceUpdate Optional Should we force - * @param forceColorscape Optional Should we force colorscape gen * @returns */ - async refreshLibraryMetadata(library: Partial, callback?: LibraryActionCallback, forceUpdate: boolean = true, forceColorscape: boolean = false) { + async refreshMetadata(library: Partial, callback?: LibraryActionCallback) { if (!library.hasOwnProperty('id') || library.id === undefined) { return; } - // Prompt the user if we are doing a forced call - if (forceUpdate) { - if (!await this.confirmService.confirm(translate('toasts.confirm-regen-covers'))) { - if (callback) { - callback(library); - } - return; + if (!await this.confirmService.confirm(translate('toasts.confirm-regen-covers'))) { + if (callback) { + callback(library); } + return; } - const message = forceUpdate ? 'toasts.refresh-covers-queued' : 'toasts.generate-colorscape-queued'; - - this.libraryService.refreshMetadata(library?.id, forceUpdate, forceColorscape).subscribe((res: any) => { - this.toastr.info(translate(message, {name: library.name})); + const forceUpdate = true; //await this.promptIfForce(); + this.libraryService.refreshMetadata(library?.id, forceUpdate).pipe(take(1)).subscribe((res: any) => { + this.toastr.info(translate('toasts.scan-queued', {name: library.name})); if (callback) { callback(library); } @@ -129,7 +102,7 @@ export class ActionService { } editLibrary(library: Partial, callback?: LibraryActionCallback) { - const modalRef = this.modalService.open(LibrarySettingsModalComponent, DefaultModalOptions); + const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); modalRef.componentInstance.library = library; modalRef.closed.subscribe((closeResult: {success: boolean, library: Library, coverImageUpdate: boolean}) => { if (callback) callback(library) @@ -162,26 +135,6 @@ export class ActionService { }); } - async deleteLibrary(library: Partial, callback?: LibraryActionCallback) { - if (!library.hasOwnProperty('id') || library.id === undefined) { - return; - } - - if (!await this.confirmService.alert(translate('toasts.confirm-library-delete'))) { - if (callback) { - callback(library); - } - return; - } - - this.libraryService.delete(library?.id).pipe(take(1)).subscribe((res: any) => { - this.toastr.info(translate('toasts.library-deleted', {name: library.name})); - if (callback) { - callback(library); - } - }); - } - /** * Mark a series as read; updates the series pagesRead * @param series Series, must have id and name populated @@ -244,25 +197,17 @@ export class ActionService { * Start a metadata refresh for a Series * @param series Series, must have libraryId, id and name populated * @param callback Optional callback to perform actions after API completes - * @param forceUpdate If cache should be checked or not - * @param forceColorscape If cache should be checked or not */ - async refreshSeriesMetadata(series: Series, callback?: SeriesActionCallback, forceUpdate: boolean = true, forceColorscape: boolean = false) { - - // Prompt the user if we are doing a forced call - if (forceUpdate) { - if (!await this.confirmService.confirm(translate('toasts.confirm-regen-covers'))) { - if (callback) { - callback(series); - } - return; + async refreshMetdata(series: Series, callback?: SeriesActionCallback) { + if (!await this.confirmService.confirm(translate('toasts.confirm-regen-covers'))) { + if (callback) { + callback(series); } + return; } - const message = forceUpdate ? 'toasts.refresh-covers-queued' : 'toasts.generate-colorscape-queued'; - - this.seriesService.refreshMetadata(series, forceUpdate, forceColorscape).pipe(take(1)).subscribe((res: any) => { - this.toastr.info(translate(message, {name: series.name})); + this.seriesService.refreshMetadata(series).pipe(take(1)).subscribe((res: any) => { + this.toastr.info(translate('toasts.refresh-covers-queued', {name: series.name})); if (callback) { callback(series); } @@ -306,7 +251,6 @@ export class ActionService { /** * Mark a chapter as read - * @param libraryId Library Id * @param seriesId Series Id * @param chapter Chapter, should have id, pages, volumeId populated * @param callback Optional callback to perform actions after API completes @@ -323,7 +267,6 @@ export class ActionService { /** * Mark a chapter as unread - * @param libraryId Library Id * @param seriesId Series Id * @param chapter Chapter, should have id, pages, volumeId populated * @param callback Optional callback to perform actions after API completes @@ -342,7 +285,7 @@ export class ActionService { * Mark all chapters and the volumes as Read. All volumes and chapters must belong to a series * @param seriesId Series Id * @param volumes Volumes, should have id, chapters and pagesRead populated - * @param chapters Optional Chapters, should have id + * @param chapters? Chapters, should have id * @param callback Optional callback to perform actions after API completes */ markMultipleAsRead(seriesId: number, volumes: Array, chapters?: Array, callback?: VoidActionCallback) { @@ -364,7 +307,6 @@ export class ActionService { * Mark all chapters and the volumes as Unread. All volumes must belong to a series * @param seriesId Series Id * @param volumes Volumes, should have id, chapters and pagesRead populated - * @param chapters Optional Chapters, should have id * @param callback Optional callback to perform actions after API completes */ markMultipleAsUnread(seriesId: number, volumes: Array, chapters?: Array, callback?: VoidActionCallback) { @@ -418,107 +360,13 @@ export class ActionService { }); } - /** - * Mark all collections as promoted/unpromoted. - * @param collections UserCollection, should have id, pagesRead populated - * @param promoted boolean, promoted state - * @param callback Optional callback to perform actions after API completes - */ - promoteMultipleCollections(collections: Array, promoted: boolean, callback?: BooleanActionCallback) { - this.collectionTagService.promoteMultipleCollections(collections.map(v => v.id), promoted).pipe(take(1)).subscribe(() => { - if (promoted) { - this.toastr.success(translate('toasts.collections-promoted')); - } else { - this.toastr.success(translate('toasts.collections-unpromoted')); - } - - if (callback) { - callback(true); - } - }); - } - - /** - * Deletes multiple collections - * @param collections UserCollection, should have id, pagesRead populated - * @param callback Optional callback to perform actions after API completes - */ - async deleteMultipleCollections(collections: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-collections'))) return; - - this.collectionTagService.deleteMultipleCollections(collections.map(v => v.id)).pipe(take(1)).subscribe(() => { - this.toastr.success(translate('toasts.collections-deleted')); - - if (callback) { - callback(true); - } - }); - } - - /** - * Mark all reading lists as promoted/unpromoted. - * @param readingLists UserCollection, should have id, pagesRead populated - * @param promoted boolean, promoted state - * @param callback Optional callback to perform actions after API completes - */ - promoteMultipleReadingLists(readingLists: Array, promoted: boolean, callback?: BooleanActionCallback) { - this.readingListService.promoteMultipleReadingLists(readingLists.map(v => v.id), promoted).pipe(take(1)).subscribe(() => { - if (promoted) { - this.toastr.success(translate('toasts.reading-list-promoted')); - } else { - this.toastr.success(translate('toasts.reading-list-unpromoted')); - } - - if (callback) { - callback(true); - } - }); - } - - async deleteMultipleVolumes(volumes: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-volumes', {count: volumes.length}))) return; - - this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => { - if (callback) { - callback(success); - } - }) - } - - async deleteMultipleChapters(seriesId: number, chapterIds: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: chapterIds.length}))) return; - - this.chapterService.deleteMultipleChapters(seriesId, chapterIds.map(c => c.id)).subscribe(() => { - if (callback) { - callback(true); - } - }); - } - - /** - * Deletes multiple collections - * @param readingLists ReadingList, should have id - * @param callback Optional callback to perform actions after API completes - */ - async deleteMultipleReadingLists(readingLists: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-reading-list'))) return; - - this.readingListService.deleteMultipleReadingLists(readingLists.map(v => v.id)).pipe(take(1)).subscribe(() => { - this.toastr.success(translate('toasts.reading-lists-deleted')); - - if (callback) { - callback(true); - } - }); - } - addMultipleToReadingList(seriesId: number, volumes: Array, chapters?: Array, callback?: BooleanActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id); this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id); - this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections'); + this.readingListModalRef.componentInstance.title = 'Multiple Selections'; this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple; @@ -538,7 +386,7 @@ export class ActionService { addMultipleSeriesToWantToReadList(seriesIds: Array, callback?: VoidActionCallback) { this.memberService.addSeriesToWantToRead(seriesIds).subscribe(() => { - this.toastr.success(translate('toasts.series-added-want-to-read')); + this.toastr.success('Series added to Want to Read list'); if (callback) { callback(); } @@ -556,9 +404,9 @@ export class ActionService { addMultipleSeriesToReadingList(series: Array, callback?: BooleanActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); this.readingListModalRef.componentInstance.seriesIds = series.map(v => v.id); - this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections'); + this.readingListModalRef.componentInstance.title = 'Multiple Selections'; this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series; @@ -584,9 +432,9 @@ export class ActionService { */ addMultipleSeriesToCollectionTag(series: Array, callback?: BooleanActionCallback) { if (this.collectionModalRef != null) { return; } - this.collectionModalRef = this.modalService.open(BulkAddToCollectionComponent, { scrollable: true, size: 'md', windowClass: 'collection', fullscreen: 'md' }); + this.collectionModalRef = this.modalService.open(BulkAddToCollectionComponent, { scrollable: true, size: 'md', windowClass: 'collection' }); this.collectionModalRef.componentInstance.seriesIds = series.map(v => v.id); - this.collectionModalRef.componentInstance.title = translate('actionable.new-collection'); + this.collectionModalRef.componentInstance.title = 'New Collection'; this.collectionModalRef.closed.pipe(take(1)).subscribe(() => { this.collectionModalRef = null; @@ -604,7 +452,7 @@ export class ActionService { addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); this.readingListModalRef.componentInstance.seriesId = series.id; this.readingListModalRef.componentInstance.title = series.name; this.readingListModalRef.componentInstance.type = ADD_FLOW.Series; @@ -626,7 +474,7 @@ export class ActionService { addVolumeToReadingList(volume: Volume, seriesId: number, callback?: VolumeActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.volumeId = volume.id; this.readingListModalRef.componentInstance.type = ADD_FLOW.Volume; @@ -648,7 +496,7 @@ export class ActionService { addChapterToReadingList(chapter: Chapter, seriesId: number, callback?: ChapterActionCallback) { if (this.readingListModalRef != null) { return; } - this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); + this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); this.readingListModalRef.componentInstance.seriesId = seriesId; this.readingListModalRef.componentInstance.chapterId = chapter.id; this.readingListModalRef.componentInstance.type = ADD_FLOW.Chapter; @@ -669,7 +517,7 @@ export class ActionService { } editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) { - const readingListModalRef = this.modalService.open(EditReadingListModalComponent, DefaultModalOptions); + const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg' }); readingListModalRef.componentInstance.readingList = readingList; readingListModalRef.closed.pipe(take(1)).subscribe((list) => { if (callback && list !== undefined) { @@ -684,9 +532,11 @@ export class ActionService { } /** - * Deletes all series - * @param seriesIds - List of series - * @param callback - Optional callback once complete + * Mark all chapters and the volumes as Read. All volumes and chapters must belong to a series + * @param seriesId Series Id + * @param volumes Volumes, should have id, chapters and pagesRead populated + * @param chapters? Chapters, should have id + * @param callback Optional callback to perform actions after API completes */ async deleteMultipleSeries(seriesIds: Array, callback?: BooleanActionCallback) { if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-series', {count: seriesIds.length}))) { @@ -695,15 +545,11 @@ export class ActionService { } return; } - this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(res => { - if (res) { - this.toastr.success(translate('toasts.series-deleted')); - } else { - this.toastr.error(translate('errors.generic')); - } + this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(() => { + this.toastr.success(translate('toasts.series-deleted')); if (callback) { - callback(res); + callback(true); } }); } @@ -718,54 +564,7 @@ export class ActionService { this.seriesService.delete(series.id).subscribe((res: boolean) => { if (callback) { - if (res) { - this.toastr.success(translate('toasts.series-deleted')); - } else { - this.toastr.error(translate('errors.generic')); - } - - callback(res); - } - }); - } - - async deleteChapter(chapterId: number, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-chapter'))) { - if (callback) { - callback(false); - } - return; - } - - this.chapterService.deleteChapter(chapterId).subscribe((res: boolean) => { - if (callback) { - if (res) { - this.toastr.success(translate('toasts.chapter-deleted')); - } else { - this.toastr.error(translate('errors.generic')); - } - - callback(res); - } - }); - } - - async deleteVolume(volumeId: number, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-volume'))) { - if (callback) { - callback(false); - } - return; - } - - this.volumeService.deleteVolume(volumeId).subscribe((res: boolean) => { - if (callback) { - if (res) { - this.toastr.success(translate('toasts.volume-deleted')); - } else { - this.toastr.error(translate('errors.generic')); - } - + this.toastr.success(translate('toasts.series-deleted')); callback(res); } }); @@ -789,83 +588,4 @@ export class ActionService { }); } - matchSeries(series: Series, callback?: BooleanActionCallback) { - const ref = this.modalService.open(MatchSeriesModalComponent, DefaultModalOptions); - ref.componentInstance.series = series; - ref.closed.subscribe(saved => { - if (callback) { - callback(saved); - } - }); - } - - async deleteFilter(filterId: number, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-smart-filter'))) { - if (callback) { - callback(false); - } - return; - } - - this.filterService.deleteFilter(filterId).subscribe(_ => { - this.toastr.success(translate('toasts.smart-filter-deleted')); - - if (callback) { - callback(true); - } - }); - } - - /** - * Sets the reading profile for multiple series - * @param series - * @param callback - */ - setReadingProfileForMultiple(series: Array, callback?: BooleanActionCallback) { - if (this.readingListModalRef != null) { return; } - - this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); - this.readingListModalRef.componentInstance.seriesIds = series.map(s => s.id) - this.readingListModalRef.componentInstance.title = "" - - this.readingListModalRef.closed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(true); - } - }); - this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(false); - } - }); - } - - /** - * Sets the reading profile for multiple series - * @param library - * @param callback - */ - setReadingProfileForLibrary(library: Library, callback?: BooleanActionCallback) { - if (this.readingListModalRef != null) { return; } - - this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); - this.readingListModalRef.componentInstance.libraryId = library.id; - this.readingListModalRef.componentInstance.title = "" - - this.readingListModalRef.closed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(true); - } - }); - this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(false); - } - }); - } - } diff --git a/UI/Web/src/app/_services/chapter.service.ts b/UI/Web/src/app/_services/chapter.service.ts deleted file mode 100644 index 6a6f7a600..000000000 --- a/UI/Web/src/app/_services/chapter.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {Injectable} from '@angular/core'; -import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; -import {Chapter} from "../_models/chapter"; -import {TextResonse} from "../_types/text-response"; -import {ChapterDetailPlus} from "../_models/chapter-detail-plus"; - -@Injectable({ - providedIn: 'root' -}) -export class ChapterService { - - baseUrl = environment.apiUrl; - - constructor(private httpClient: HttpClient) { } - - getChapterMetadata(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'chapter?chapterId=' + chapterId); - } - - deleteChapter(chapterId: number) { - return this.httpClient.delete(this.baseUrl + 'chapter?chapterId=' + chapterId); - } - - deleteMultipleChapters(seriesId: number, chapterIds: Array) { - return this.httpClient.post(this.baseUrl + `chapter/delete-multiple?seriesId=${seriesId}`, {chapterIds}); - } - - updateChapter(chapter: Chapter) { - return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse); - } - - chapterDetailPlus(seriesId: number, chapterId: number) { - return this.httpClient.get(this.baseUrl + `chapter/chapter-detail-plus?chapterId=${chapterId}&seriesId=${seriesId}`); - } - -} diff --git a/UI/Web/src/app/_services/collection-tag.service.ts b/UI/Web/src/app/_services/collection-tag.service.ts index df668f13a..3e4b8b508 100644 --- a/UI/Web/src/app/_services/collection-tag.service.ts +++ b/UI/Web/src/app/_services/collection-tag.service.ts @@ -1,12 +1,10 @@ import { HttpClient } from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {environment} from 'src/environments/environment'; -import {UserCollection} from '../_models/collection-tag'; -import {TextResonse} from '../_types/text-response'; -import {MalStack} from "../_models/collection/mal-stack"; -import {Action, ActionItem} from "./action-factory.service"; -import {User} from "../_models/user"; -import {AccountService} from "./account.service"; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { CollectionTag } from '../_models/collection-tag'; +import { TextResonse } from '../_types/text-response'; +import { ImageService } from './image.service'; @Injectable({ providedIn: 'root' @@ -15,25 +13,24 @@ export class CollectionTagService { baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient, private accountService: AccountService) { } + constructor(private httpClient: HttpClient, private imageService: ImageService) { } - allCollections(ownedOnly = false) { - return this.httpClient.get(this.baseUrl + 'collection?ownedOnly=' + ownedOnly); + allTags() { + return this.httpClient.get(this.baseUrl + 'collection/'); } - allCollectionsForSeries(seriesId: number, ownedOnly = false) { - return this.httpClient.get(this.baseUrl + 'collection/all-series?ownedOnly=' + ownedOnly + '&seriesId=' + seriesId); + search(query: string) { + return this.httpClient.get(this.baseUrl + 'collection/search?queryString=' + encodeURIComponent(query)).pipe(map(tags => { + tags.forEach(s => s.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(s.id))); + return tags; + })); } - updateTag(tag: UserCollection) { + updateTag(tag: CollectionTag) { return this.httpClient.post(this.baseUrl + 'collection/update', tag, TextResonse); } - promoteMultipleCollections(tags: Array, promoted: boolean) { - return this.httpClient.post(this.baseUrl + 'collection/promote-multiple', {collectionIds: tags, promoted}, TextResonse); - } - - updateSeriesForTag(tag: UserCollection, seriesIdsToRemove: Array) { + updateSeriesForTag(tag: CollectionTag, seriesIdsToRemove: Array) { return this.httpClient.post(this.baseUrl + 'collection/update-series', {tag, seriesIdsToRemove}, TextResonse); } @@ -48,24 +45,4 @@ export class CollectionTagService { deleteTag(tagId: number) { return this.httpClient.delete(this.baseUrl + 'collection?tagId=' + tagId, TextResonse); } - - deleteMultipleCollections(tags: Array) { - return this.httpClient.post(this.baseUrl + 'collection/delete-multiple', {collectionIds: tags}, TextResonse); - } - - getMalStacks() { - return this.httpClient.get>(this.baseUrl + 'collection/mal-stacks'); - } - - actionListFilter(action: ActionItem, user: User) { - const canPromote = this.accountService.hasAdminRole(user) || this.accountService.hasPromoteRole(user); - const isPromotionAction = action.action == Action.Promote || action.action == Action.UnPromote; - - if (isPromotionAction) return canPromote; - return true; - } - - importStack(stack: MalStack) { - return this.httpClient.post(this.baseUrl + 'collection/import-stack', stack, TextResonse); - } } diff --git a/UI/Web/src/app/_services/colorscape.service.ts b/UI/Web/src/app/_services/colorscape.service.ts deleted file mode 100644 index 88cbd7460..000000000 --- a/UI/Web/src/app/_services/colorscape.service.ts +++ /dev/null @@ -1,511 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; -import { DOCUMENT } from '@angular/common'; -import {BehaviorSubject, filter, take, tap, timer} from 'rxjs'; -import {NavigationEnd, Router} from "@angular/router"; - -interface ColorSpace { - primary: string; - lighter: string; - darker: string; - complementary: string; -} - -interface ColorSpaceRGBA { - primary: RGBAColor; - lighter: RGBAColor; - darker: RGBAColor; - complementary: RGBAColor; -} - -interface RGBAColor {r: number;g: number;b: number;a: number;} -interface RGB { r: number;g: number; b: number; } -interface HSL { h: number; s: number; l: number; } - -const colorScapeSelector = 'colorscape'; - -/** - * ColorScape handles setting the scape and managing the transitions - */ -@Injectable({ - providedIn: 'root' -}) -export class ColorscapeService { - private colorSubject = new BehaviorSubject(null); - private colorSeedSubject = new BehaviorSubject<{primary: string, complementary: string | null} | null>(null); - public readonly colors$ = this.colorSubject.asObservable(); - - private minDuration = 1000; // minimum duration - private maxDuration = 4000; // maximum duration - private defaultColorspaceDuration = 300; // duration to wait before defaulting back to default colorspace - - constructor(@Inject(DOCUMENT) private document: Document, private readonly router: Router) { - this.router.events.pipe( - filter(event => event instanceof NavigationEnd), - tap(() => this.checkAndResetColorscapeAfterDelay()) - ).subscribe(); - - } - - /** - * Due to changing ColorScape on route end, we might go from one space to another, but the router events resets to default - * This delays it to see if the colors changed or not in 500ms and if not, then we will reset to default. - * @private - */ - private checkAndResetColorscapeAfterDelay() { - // Capture the current colors at the start of NavigationEnd - const initialColors = this.colorSubject.getValue(); - - // Wait for X ms, then check if colors have changed - timer(this.defaultColorspaceDuration).pipe( - take(1), // Complete after the timer emits - tap(() => { - const currentColors = this.colorSubject.getValue(); - if (initialColors != null && currentColors != null && this.areColorSpacesVisuallyEqual(initialColors, currentColors)) { - this.setColorScape(''); // Reset to default if colors haven't changed - } - }) - ).subscribe(); - } - - /** - * Sets a color scape for the active theme - * @param primaryColor - * @param complementaryColor - */ - setColorScape(primaryColor: string, complementaryColor: string | null = null) { - if (this.getCssVariable('--colorscape-enabled') === 'false') { - return; - } - - const elem = this.document.querySelector('#backgroundCanvas'); - - if (!elem) { - return; - } - - // Check the old seed colors and check if they are similar, then avoid a change. In case you scan a series and this re-generates - const previousColors = this.colorSeedSubject.getValue(); - if (previousColors != null && primaryColor == previousColors.primary) { - this.colorSeedSubject.next({primary: primaryColor, complementary: complementaryColor}); - return; - } - this.colorSeedSubject.next({primary: primaryColor, complementary: complementaryColor}); - - // TODO: Check if there is a secondary color and if the color is a strong contrast (opposite on color wheel) to primary - - // If we have a STRONG primary and secondary, generate LEFT/RIGHT orientation - // If we have only one color, randomize the position of the primary - // If we have 2 colors, but their contrast isn't STRONG, then use diagonal for Primary and Secondary - - - - - const newColors: ColorSpace = primaryColor ? - this.generateBackgroundColors(primaryColor, complementaryColor, this.isDarkTheme()) : - this.defaultColors(); - - const newColorsRGBA = this.convertColorsToRGBA(newColors); - const oldColors = this.colorSubject.getValue() || this.convertColorsToRGBA(this.defaultColors()); - const duration = this.calculateTransitionDuration(oldColors, newColorsRGBA); - - // Check if the colors we are transitioning to are visually equal - if (this.areColorSpacesVisuallyEqual(oldColors, newColorsRGBA)) { - return; - } - - this.animateColorTransition(oldColors, newColorsRGBA, duration); - - this.colorSubject.next(newColorsRGBA); - } - - private areColorSpacesVisuallyEqual(color1: ColorSpaceRGBA, color2: ColorSpaceRGBA, threshold: number = 0): boolean { - return this.areRGBAColorsVisuallyEqual(color1.primary, color2.primary, threshold) && - this.areRGBAColorsVisuallyEqual(color1.lighter, color2.lighter, threshold) && - this.areRGBAColorsVisuallyEqual(color1.darker, color2.darker, threshold) && - this.areRGBAColorsVisuallyEqual(color1.complementary, color2.complementary, threshold); - } - - private areRGBAColorsVisuallyEqual(color1: RGBAColor, color2: RGBAColor, threshold: number = 0): boolean { - return Math.abs(color1.r - color2.r) <= threshold && - Math.abs(color1.g - color2.g) <= threshold && - Math.abs(color1.b - color2.b) <= threshold && - Math.abs(color1.a - color2.a) <= threshold / 255; - } - - private convertColorsToRGBA(colors: ColorSpace): ColorSpaceRGBA { - return { - primary: this.parseColorToRGBA(colors.primary), - lighter: this.parseColorToRGBA(colors.lighter), - darker: this.parseColorToRGBA(colors.darker), - complementary: this.parseColorToRGBA(colors.complementary) - }; - } - - private parseColorToRGBA(color: string) { - - if (color.startsWith('#')) { - return this.hexToRGBA(color); - } else if (color.startsWith('rgb')) { - return this.rgbStringToRGBA(color); - } else { - console.warn(`Unsupported color format: ${color}. Defaulting to black.`); - return { r: 0, g: 0, b: 0, a: 1 }; - } - } - - private hexToRGBA(hex: string, opacity: number = 1): RGBAColor { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - a: opacity - } - : { r: 0, g: 0, b: 0, a: opacity }; - } - - private rgbStringToRGBA(rgb: string): RGBAColor { - const matches = rgb.match(/(\d+(\.\d+)?)/g); - if (matches) { - return { - r: parseInt(matches[0], 10), - g: parseInt(matches[1], 10), - b: parseInt(matches[2], 10), - a: matches.length === 4 ? parseFloat(matches[3]) : 1 - }; - } - return { r: 0, g: 0, b: 0, a: 1 }; - } - - private calculateTransitionDuration(oldColors: ColorSpaceRGBA, newColors: ColorSpaceRGBA): number { - const colorKeys: (keyof ColorSpaceRGBA)[] = ['primary', 'lighter', 'darker', 'complementary']; - let totalDistance = 0; - - for (const key of colorKeys) { - const oldRGB = this.rgbaToRgb(oldColors[key]); - const newRGB = this.rgbaToRgb(newColors[key]); - totalDistance += this.calculateColorDistance(oldRGB, newRGB); - } - - // Normalize the total distance and map it to our duration range - const normalizedDistance = Math.min(totalDistance / (255 * 3 * 4), 1); // Max possible distance is 255*3*4 - const duration = this.minDuration + normalizedDistance * (this.maxDuration - this.minDuration); - - // Add random variance to the duration - const durationVariance = this.getRandomInRange(-500, 500); - - return Math.round(duration + durationVariance); - } - - private rgbaToRgb(rgba: RGBAColor): RGB { - return { r: rgba.r, g: rgba.g, b: rgba.b }; - } - - private calculateColorDistance(rgb1: RGB, rgb2: RGB): number { - return Math.sqrt( - Math.pow(rgb2.r - rgb1.r, 2) + - Math.pow(rgb2.g - rgb1.g, 2) + - Math.pow(rgb2.b - rgb1.b, 2) - ); - } - - - private defaultColors() { - return { - primary: this.getCssVariable('--colorscape-primary-default-color'), - lighter: this.getCssVariable('--colorscape-lighter-default-color'), - darker: this.getCssVariable('--colorscape-darker-default-color'), - complementary: this.getCssVariable('--colorscape-complementary-default-color'), - } - } - - private animateColorTransition(oldColors: ColorSpaceRGBA, newColors: ColorSpaceRGBA, duration: number) { - const startTime = performance.now(); - - const animate = (currentTime: number) => { - const elapsedTime = currentTime - startTime; - const progress = Math.min(elapsedTime / duration, 1); - - const interpolatedColors: ColorSpaceRGBA = { - primary: this.interpolateRGBAColor(oldColors.primary, newColors.primary, progress), - lighter: this.interpolateRGBAColor(oldColors.lighter, newColors.lighter, progress), - darker: this.interpolateRGBAColor(oldColors.darker, newColors.darker, progress), - complementary: this.interpolateRGBAColor(oldColors.complementary, newColors.complementary, progress) - }; - - this.setColorsImmediately(interpolatedColors); - - if (progress < 1) { - requestAnimationFrame(animate); - } - }; - - requestAnimationFrame(animate); - } - - private easeInOutCubic(t: number): number { - return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; - } - - private interpolateRGBAColor(color1: RGBAColor, color2: RGBAColor, progress: number): RGBAColor { - - const easedProgress = this.easeInOutCubic(progress); - - return { - r: Math.round(color1.r + (color2.r - color1.r) * easedProgress), - g: Math.round(color1.g + (color2.g - color1.g) * easedProgress), - b: Math.round(color1.b + (color2.b - color1.b) * easedProgress), - a: color1.a + (color2.a - color1.a) * easedProgress - }; - } - - private setColorsImmediately(colors: ColorSpaceRGBA) { - this.injectStyleElement(colorScapeSelector, ` - :root, :root .default { - --colorscape-primary-color: ${this.rgbToString(colors.primary)}; - --colorscape-lighter-color: ${this.rgbToString(colors.lighter)}; - --colorscape-darker-color: ${this.rgbToString(colors.darker)}; - --colorscape-complementary-color: ${this.rgbToString(colors.complementary)}; - --colorscape-primary-no-alpha-color: ${this.rgbaToString({ ...colors.primary, a: 0 })}; - --colorscape-lighter-no-alpha-color: ${this.rgbaToString({ ...colors.lighter, a: 0 })}; - --colorscape-darker-no-alpha-color: ${this.rgbaToString({ ...colors.darker, a: 0 })}; - --colorscape-complementary-no-alpha-color: ${this.rgbaToString({ ...colors.complementary, a: 0 })}; - --colorscape-primary-full-alpha-color: ${this.rgbaToString({ ...colors.primary, a: 1 })}; - --colorscape-lighter-full-alpha-color: ${this.rgbaToString({ ...colors.lighter, a: 1 })}; - --colorscape-darker-full-alpha-color: ${this.rgbaToString({ ...colors.darker, a: 1 })}; - --colorscape-complementary-full-alpha-color: ${this.rgbaToString({ ...colors.complementary, a: 1 })}; - --colorscape-primary-half-alpha-color: ${this.rgbaToString({ ...colors.primary, a: 0.5 })}; - --colorscape-lighter-half-alpha-color: ${this.rgbaToString({ ...colors.lighter, a: 0.5 })}; - --colorscape-darker-half-alpha-color: ${this.rgbaToString({ ...colors.darker, a: 0.5 })}; - --colorscape-complementary-half-alpha-color: ${this.rgbaToString({ ...colors.complementary, a: 0.5 })}; - } - `); - } - - private generateBackgroundColors(primaryColor: string, secondaryColor: string | null = null, isDarkTheme: boolean = true): ColorSpace { - const primary = this.hexToRgb(primaryColor); - const secondary = secondaryColor ? this.hexToRgb(secondaryColor) : this.calculateComplementaryRgb(primary); - - const primaryHSL = this.rgbToHsl(primary); - const secondaryHSL = this.rgbToHsl(secondary); - - return isDarkTheme - ? this.calculateDarkThemeColors(secondaryHSL, primaryHSL, primary) - : this.calculateLightThemeDarkColors(primaryHSL, primary); // NOTE: Light themes look bad in general with this system. - } - - private adjustColorWithVariance(color: string): string { - const rgb = this.hexToRgb(color); - const randomVariance = () => this.getRandomInRange(-10, 10); // Random variance for each color channel - return this.rgbToHex({ - r: Math.min(255, Math.max(0, rgb.r + randomVariance())), - g: Math.min(255, Math.max(0, rgb.g + randomVariance())), - b: Math.min(255, Math.max(0, rgb.b + randomVariance())) - }); - } - - private calculateLightThemeDarkColors(primaryHSL: HSL, primary: RGB) { - const lighterHSL = {...primaryHSL}; - lighterHSL.s = Math.max(lighterHSL.s - 0.3, 0); - lighterHSL.l = Math.min(lighterHSL.l + 0.5, 0.95); - - const darkerHSL = {...primaryHSL}; - darkerHSL.s = Math.max(darkerHSL.s - 0.1, 0); - darkerHSL.l = Math.min(darkerHSL.l + 0.3, 0.9); - - const complementaryHSL = this.adjustHue(primaryHSL, 180); - complementaryHSL.s = Math.max(complementaryHSL.s - 0.2, 0); - complementaryHSL.l = Math.min(complementaryHSL.l + 0.4, 0.9); - - return { - primary: this.rgbToHex(primary), - lighter: this.rgbToHex(this.hslToRgb(lighterHSL)), - darker: this.rgbToHex(this.hslToRgb(darkerHSL)), - complementary: this.rgbToHex(this.hslToRgb(complementaryHSL)) - }; - } - - private calculateDarkThemeColors(secondaryHSL: HSL, primaryHSL: { - h: number; - s: number; - l: number - }, primary: RGB) { - const lighterHSL = this.adjustHue(secondaryHSL, 30); - lighterHSL.s = Math.min(lighterHSL.s + 0.2, 1); - lighterHSL.l = Math.min(lighterHSL.l + 0.1, 0.6); - - const darkerHSL = {...primaryHSL}; - darkerHSL.l = Math.max(darkerHSL.l - 0.3, 0.1); - - const complementaryHSL = this.adjustHue(primaryHSL, 180); - complementaryHSL.s = Math.min(complementaryHSL.s + 0.1, 1); - complementaryHSL.l = Math.max(complementaryHSL.l - 0.2, 0.2); - - // Array of colors to shuffle - const colors = [ - this.rgbToHex(primary), - this.rgbToHex(this.hslToRgb(lighterHSL)), - this.rgbToHex(this.hslToRgb(darkerHSL)), - this.rgbToHex(this.hslToRgb(complementaryHSL)) - ]; - - // Shuffle colors array - this.shuffleArray(colors); - - // Set a brightness threshold (you can adjust this value as needed) - const brightnessThreshold = 100; // Adjust based on your needs (0-255) - - // Ensure the 'lighter' color is not too bright - if (this.getBrightness(colors[1]) > brightnessThreshold) { - // If it is too bright, find a suitable swap - for (let i = 0; i < colors.length; i++) { - if (this.getBrightness(colors[i]) <= brightnessThreshold) { - // Swap colors[1] (lighter) with a less bright color - [colors[1], colors[i]] = [colors[i], colors[1]]; - break; - } - } - } - - // Ensure no color is repeating and variance is maintained - const uniqueColors = new Set(colors); - if (uniqueColors.size < colors.length) { - // If there are duplicates, re-shuffle the array - this.shuffleArray(colors); - } - - return { - primary: colors[0], - lighter: colors[1], - darker: colors[2], - complementary: colors[3] - }; - } - - // Calculate brightness of a color (RGB) - private getBrightness(color: string) { - const rgb = this.hexToRgb(color); // Convert hex to RGB - // Using the luminance formula for brightness - return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b); - } - - // Fisher-Yates shuffle algorithm - private shuffleArray(array: string[]) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - } - - private hexToRgb(hex: string): RGB { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : { r: 0, g: 0, b: 0 }; - } - - private rgbToHex(rgb: RGB): string { - return `#${((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1)}`; - } - - private rgbToHsl(rgb: RGB): HSL { - const r = rgb.r / 255; - const g = rgb.g / 255; - const b = rgb.b / 255; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - let h = 0; - let s = 0; - const l = (max + min) / 2; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } - h /= 6; - } - - return { h, s, l }; - } - - private hslToRgb(hsl: HSL): RGB { - const { h, s, l } = hsl; - let r, g, b; - - if (s === 0) { - r = g = b = l; - } else { - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1/6) return p + (q - p) * 6 * t; - if (t < 1/2) return q; - if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; - return p; - }; - - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - r = hue2rgb(p, q, h + 1/3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1/3); - } - - return { - r: Math.round(r * 255), - g: Math.round(g * 255), - b: Math.round(b * 255) - }; - } - - private adjustHue(hsl: HSL, amount: number): HSL { - return { - h: (hsl.h + amount / 360) % 1, - s: hsl.s, - l: hsl.l - }; - } - - private calculateComplementaryRgb(rgb: RGB): RGB { - const hsl = this.rgbToHsl(rgb); - const complementaryHsl = this.adjustHue(hsl, 180); - return this.hslToRgb(complementaryHsl); - } - - private rgbaToString(color: RGBAColor): string { - return `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; - } - - private rgbToString(color: RGBAColor): string { - return `rgb(${color.r}, ${color.g}, ${color.b})`; - } - - private getCssVariable(variableName: string): string { - return getComputedStyle(this.document.body).getPropertyValue(variableName).trim(); - } - - private isDarkTheme(): boolean { - return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim().toLowerCase() === 'dark'; - } - - private injectStyleElement(id: string, styles: string) { - let styleElement = this.document.getElementById(id); - if (!styleElement) { - styleElement = this.document.createElement('style'); - styleElement.id = id; - this.document.head.appendChild(styleElement); - } - styleElement.textContent = styles; - } - - private getRandomInRange(min: number, max: number): number { - return Math.random() * (max - min) + min; - } -} diff --git a/UI/Web/src/app/_services/dashboard.service.ts b/UI/Web/src/app/_services/dashboard.service.ts index 493fae370..9d9deeffb 100644 --- a/UI/Web/src/app/_services/dashboard.service.ts +++ b/UI/Web/src/app/_services/dashboard.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; import {TextResonse} from "../_types/text-response"; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; @@ -26,8 +26,4 @@ export class DashboardService { createDashboardStream(smartFilterId: number) { return this.httpClient.post(this.baseUrl + 'stream/add-dashboard-stream?smartFilterId=' + smartFilterId, {}); } - - deleteSmartFilterStream(streamId: number) { - return this.httpClient.delete(this.baseUrl + 'stream/smart-filter-dashboard-stream?dashboardStreamId=' + streamId, {}); - } } diff --git a/UI/Web/src/app/_services/device.service.ts b/UI/Web/src/app/_services/device.service.ts index 496abf9c2..1ba491177 100644 --- a/UI/Web/src/app/_services/device.service.ts +++ b/UI/Web/src/app/_services/device.service.ts @@ -33,11 +33,11 @@ export class DeviceService { } createDevice(name: string, platform: DevicePlatform, emailAddress: string) { - return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}); + return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}, TextResonse); } updateDevice(id: number, name: string, platform: DevicePlatform, emailAddress: string) { - return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}); + return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}, TextResonse); } deleteDevice(id: number) { diff --git a/UI/Web/src/app/_services/email.service.ts b/UI/Web/src/app/_services/email.service.ts deleted file mode 100644 index 5afb62ca7..000000000 --- a/UI/Web/src/app/_services/email.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; -import {EmailHistory} from "../_models/email-history"; - -@Injectable({ - providedIn: 'root' -}) -export class EmailService { - baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } - - getEmailHistory() { - return this.httpClient.get(`${this.baseUrl}email/all`); - } -} diff --git a/UI/Web/src/app/_services/filter.service.ts b/UI/Web/src/app/_services/filter.service.ts index 2b9681e90..7d2648072 100644 --- a/UI/Web/src/app/_services/filter.service.ts +++ b/UI/Web/src/app/_services/filter.service.ts @@ -1,7 +1,8 @@ -import {Injectable} from '@angular/core'; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import { Injectable } from '@angular/core'; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; +import {JumpKey} from "../_models/jumpbar/jump-key"; import {SmartFilter} from "../_models/metadata/v2/smart-filter"; @Injectable({ @@ -12,7 +13,7 @@ export class FilterService { baseUrl = environment.apiUrl; constructor(private httpClient: HttpClient) { } - saveFilter(filter: FilterV2) { + saveFilter(filter: SeriesFilterV2) { return this.httpClient.post(this.baseUrl + 'filter/update', filter); } getAllFilters() { @@ -22,7 +23,4 @@ export class FilterService { return this.httpClient.delete(this.baseUrl + 'filter?filterId=' + filterId); } - renameSmartFilter(filter: SmartFilter) { - return this.httpClient.post(this.baseUrl + `filter/rename?filterId=${filter.id}&name=${filter.name.trim()}`, {}); - } } diff --git a/UI/Web/src/app/_services/image.service.ts b/UI/Web/src/app/_services/image.service.ts index 86aa8872a..4aea05982 100644 --- a/UI/Web/src/app/_services/image.service.ts +++ b/UI/Web/src/app/_services/image.service.ts @@ -17,21 +17,18 @@ export class ImageService { public errorImage = 'assets/images/error-placeholder2.dark-min.png'; public resetCoverImage = 'assets/images/image-reset-cover-min.png'; public errorWebLinkImage = 'assets/images/broken-white-32x32.png'; - public nextChapterImage = 'assets/images/image-placeholder.dark-min.png'; - public noPersonImage = 'assets/images/error-person-missing.dark.min.png'; + public nextChapterImage = 'assets/images/image-placeholder.dark-min.png' constructor(private accountService: AccountService, private themeService: ThemeService) { this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => { if (this.themeService.isDarkTheme()) { this.placeholderImage = 'assets/images/image-placeholder.dark-min.png'; this.errorImage = 'assets/images/error-placeholder2.dark-min.png'; - this.errorWebLinkImage = 'assets/images/broken-black-32x32.png'; - this.noPersonImage = 'assets/images/error-person-missing.dark.min.png'; + this.errorWebLinkImage = 'assets/images/broken-white-32x32.png'; } else { this.placeholderImage = 'assets/images/image-placeholder-min.png'; this.errorImage = 'assets/images/error-placeholder2-min.png'; - this.errorWebLinkImage = 'assets/images/broken-white-32x32.png'; - this.noPersonImage = 'assets/images/error-person-missing.min.png'; + this.errorWebLinkImage = 'assets/images/broken-black-32x32.png'; } }); @@ -62,13 +59,6 @@ export class ImageService { return part.substring(0, equalIndex).replace('Id', ''); } - getPersonImage(personId: number) { - return `${this.baseUrl}image/person-cover?personId=${personId}&apiKey=${this.encodedKey}`; - } - getPersonImageByName(name: string) { - return `${this.baseUrl}image/person-cover-by-name?name=${name}&apiKey=${this.encodedKey}`; - } - getLibraryCoverImage(libraryId: number) { return `${this.baseUrl}image/library-cover?libraryId=${libraryId}&apiKey=${this.encodedKey}`; } @@ -101,14 +91,14 @@ export class ImageService { return `${this.baseUrl}image/web-link?url=${encodeURIComponent(url)}&apiKey=${this.encodedKey}`; } - getPublisherImage(name: string) { - return `${this.baseUrl}image/publisher?publisherName=${encodeURIComponent(name)}&apiKey=${this.encodedKey}`; - } - getCoverUploadImage(filename: string) { return `${this.baseUrl}image/cover-upload?filename=${encodeURIComponent(filename)}&apiKey=${this.encodedKey}`; } + updateErroredImage(event: any) { + event.target.src = this.placeholderImage; + } + updateErroredWebLinkImage(event: any) { event.target.src = this.errorWebLinkImage; } diff --git a/UI/Web/src/app/_services/jumpbar.service.ts b/UI/Web/src/app/_services/jumpbar.service.ts index 48ca08705..6ae2cb2e2 100644 --- a/UI/Web/src/app/_services/jumpbar.service.ts +++ b/UI/Web/src/app/_services/jumpbar.service.ts @@ -1,5 +1,5 @@ -import {Injectable} from '@angular/core'; -import {JumpKey} from '../_models/jumpbar/jump-key'; +import { Injectable } from '@angular/core'; +import { JumpKey } from '../_models/jumpbar/jump-key'; const keySize = 25; // Height of the JumpBar button @@ -16,23 +16,21 @@ export class JumpbarService { getResumeKey(key: string) { - const k = key.toUpperCase(); - if (this.resumeKeys.hasOwnProperty(k)) return this.resumeKeys[k]; + if (this.resumeKeys.hasOwnProperty(key)) return this.resumeKeys[key]; return ''; } - getResumePosition(url: string) { - if (this.resumeScroll.hasOwnProperty(url)) return this.resumeScroll[url]; + getResumePosition(key: string) { + if (this.resumeScroll.hasOwnProperty(key)) return this.resumeScroll[key]; return 0; } saveResumeKey(key: string, value: string) { - const k = key.toUpperCase(); - this.resumeKeys[k] = value; + this.resumeKeys[key] = value; } - saveResumePosition(url: string, value: number) { - this.resumeScroll[url] = value; + saveScrollOffset(key: string, value: number) { + this.resumeScroll[key] = value; } generateJumpBar(jumpBarKeys: Array, currentSize: number) { @@ -77,11 +75,9 @@ export class JumpbarService { _removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { const removedIndexes: Array = []; - for(let removal = 0; removal < numberOfRemovals; removal++) { let min = 100000000; let minIndex = -1; - for(let i = 1; i < midPoint; i++) { if (jumpBarKeys[i].size < min && !removedIndexes.includes(i)) { min = jumpBarKeys[i].size; @@ -97,33 +93,28 @@ export class JumpbarService { } /** - * + * * @param data An array of objects * @param keySelector A method to fetch a string from the object, which is used to classify the JumpKey - * @returns + * @returns */ getJumpKeys(data :Array, keySelector: (data: any) => string) { const keys: {[key: string]: number} = {}; data.forEach(obj => { - try { - let ch = keySelector(obj).charAt(0).toUpperCase(); - if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { - ch = '#'; - } - if (!keys.hasOwnProperty(ch)) { - keys[ch] = 0; - } - keys[ch] += 1; - } catch (e) { - console.error('Failed to calculate jump key for ', obj, e); + let ch = keySelector(obj).charAt(0); + if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { + ch = '#'; } + if (!keys.hasOwnProperty(ch)) { + keys[ch] = 0; + } + keys[ch] += 1; }); return Object.keys(keys).map(k => { - k = k.toUpperCase(); return { key: k, size: keys[k], - title: k + title: k.toUpperCase() } }).sort((a, b) => { if (a.key < b.key) return -1; diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 8c851dd80..2d9edd190 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -1,13 +1,11 @@ import { HttpClient } from '@angular/common/http'; -import {DestroyRef, Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; import { of } from 'rxjs'; -import {filter, map, tap} from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { JumpKey } from '../_models/jumpbar/jump-key'; -import { Library, LibraryType } from '../_models/library/library'; +import { Library, LibraryType } from '../_models/library'; import { DirectoryDto } from '../_models/system/directory-dto'; -import {EVENTS, MessageHubService} from "./message-hub.service"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @Injectable({ @@ -20,21 +18,14 @@ export class LibraryService { private libraryNames: {[key:number]: string} | undefined = undefined; private libraryTypes: {[key: number]: LibraryType} | undefined = undefined; - constructor(private httpClient: HttpClient, private readonly messageHub: MessageHubService, private readonly destroyRef: DestroyRef) { - this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(e => e.event === EVENTS.LibraryModified), - tap((e) => { - console.log('LibraryModified event came in, clearing library name cache'); - this.libraryNames = undefined; - this.libraryTypes = undefined; - })).subscribe(); - } + constructor(private httpClient: HttpClient) {} getLibraryNames() { if (this.libraryNames != undefined) { return of(this.libraryNames); } - return this.httpClient.get(this.baseUrl + 'library/libraries').pipe(map(libraries => { + return this.httpClient.get(this.baseUrl + 'library').pipe(map(libraries => { this.libraryNames = {}; libraries.forEach(lib => { if (this.libraryNames !== undefined) { @@ -49,7 +40,7 @@ export class LibraryService { if (this.libraryNames != undefined && this.libraryNames.hasOwnProperty(libraryId)) { return of(this.libraryNames[libraryId]); } - return this.httpClient.get(this.baseUrl + 'library/libraries').pipe(map(l => { + return this.httpClient.get(this.baseUrl + 'library').pipe(map(l => { this.libraryNames = {}; l.forEach(lib => { if (this.libraryNames !== undefined) { @@ -77,12 +68,8 @@ export class LibraryService { return this.httpClient.get(this.baseUrl + 'library/jump-bar?libraryId=' + libraryId); } - getLibrary(libraryId: number) { - return this.httpClient.get(this.baseUrl + 'library?libraryId=' + libraryId); - } - getLibraries() { - return this.httpClient.get(this.baseUrl + 'library/libraries'); + return this.httpClient.get(this.baseUrl + 'library'); } updateLibrariesForMember(username: string, selectedLibraries: Library[]) { @@ -93,28 +80,12 @@ export class LibraryService { return this.httpClient.post(this.baseUrl + 'library/scan?libraryId=' + libraryId + '&force=' + force, {}); } - scanMultipleLibraries(libraryIds: Array, force = false) { - return this.httpClient.post(this.baseUrl + 'library/scan-multiple', {ids: libraryIds, force: force}); - } - analyze(libraryId: number) { return this.httpClient.post(this.baseUrl + 'library/analyze?libraryId=' + libraryId, {}); } - refreshMetadata(libraryId: number, forceUpdate = false, forceColorscape = false) { - return this.httpClient.post(this.baseUrl + `library/refresh-metadata?libraryId=${libraryId}&force=${forceUpdate}&forceColorscape=${forceColorscape}`, {}); - } - - refreshMetadataMultipleLibraries(libraryIds: Array, force = false, forceColorscape = false) { - return this.httpClient.post(this.baseUrl + 'library/refresh-metadata-multiple?forceColorscape=' + forceColorscape, {ids: libraryIds, force: force}); - } - - analyzeFilesMultipleLibraries(libraryIds: Array) { - return this.httpClient.post(this.baseUrl + 'library/analyze-multiple', {ids: libraryIds, force: false}); - } - - copySettingsFromLibrary(sourceLibraryId: number, targetLibraryIds: Array, includeType: boolean) { - return this.httpClient.post(this.baseUrl + 'library/copy-settings-from', {sourceLibraryId, targetLibraryIds, includeType}); + refreshMetadata(libraryId: number, forceUpdate = false) { + return this.httpClient.post(this.baseUrl + 'library/refresh-metadata?libraryId=' + libraryId + '&force=' + forceUpdate, {}); } create(model: {name: string, type: number, folders: string[]}) { @@ -125,13 +96,6 @@ export class LibraryService { return this.httpClient.delete(this.baseUrl + 'library/delete?libraryId=' + libraryId, {}); } - deleteMultiple(libraryIds: Array) { - if (libraryIds.length === 0) { - return of(); - } - return this.httpClient.delete(this.baseUrl + 'library/delete-multiple?libraryIds=' + libraryIds.join(','), {}); - } - update(model: {name: string, folders: string[], id: number}) { return this.httpClient.post(this.baseUrl + 'library/update', model); } diff --git a/UI/Web/src/app/_services/license.service.ts b/UI/Web/src/app/_services/license.service.ts deleted file mode 100644 index a2e77f2fe..000000000 --- a/UI/Web/src/app/_services/license.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {HttpClient} from "@angular/common/http"; -import {catchError, map, ReplaySubject, tap, throwError} from "rxjs"; -import {environment} from "../../environments/environment"; -import {TextResonse} from '../_types/text-response'; -import {LicenseInfo} from "../_models/kavitaplus/license-info"; - -@Injectable({ - providedIn: 'root' -}) -export class LicenseService { - private readonly httpClient = inject(HttpClient); - - baseUrl = environment.apiUrl; - - private readonly hasValidLicenseSource = new ReplaySubject(1); - /** - * Does the user have an active license - */ - public readonly hasValidLicense$ = this.hasValidLicenseSource.asObservable(); - - - /** - * Delete the license from the server and update hasValidLicenseSource to false - */ - deleteLicense() { - return this.httpClient.delete(this.baseUrl + 'license', TextResonse).pipe( - map(res => res === "true"), - tap(_ => { - this.hasValidLicenseSource.next(false) - }), - catchError(error => { - this.hasValidLicenseSource.next(false); - return throwError(error); // Rethrow the error to propagate it further - }) - ); - } - - resetLicense(license: string, email: string) { - return this.httpClient.post(this.baseUrl + 'license/reset', {license, email}, TextResonse); - } - - /** - * Returns information about License and will internally cache if license is valid or not - */ - licenseInfo(forceCheck: boolean = false) { - return this.httpClient.get(this.baseUrl + `license/info?forceCheck=${forceCheck}`).pipe( - tap(res => { - this.hasValidLicenseSource.next(res?.isActive || false) - }), - catchError(error => { - this.hasValidLicenseSource.next(false); - return throwError(error); // Rethrow the error to propagate it further - }) - ); - } - - hasValidLicense(forceCheck: boolean = false) { - return this.httpClient.get(this.baseUrl + 'license/valid-license?forceCheck=' + forceCheck, TextResonse) - .pipe( - map(res => res === "true"), - tap(res => { - this.hasValidLicenseSource.next(res) - }), - catchError(error => { - this.hasValidLicenseSource.next(false); - return throwError(error); // Rethrow the error to propagate it further - }) - ); - } - - hasAnyLicense() { - return this.httpClient.get(this.baseUrl + 'license/has-license', TextResonse) - .pipe( - map(res => res === "true"), - ); - } - - updateUserLicense(license: string, email: string, discordId?: string) { - return this.httpClient.post(this.baseUrl + 'license', {license, email, discordId}, TextResonse) - .pipe(map(res => res === "true")); - } -} diff --git a/UI/Web/src/app/_services/localization.service.ts b/UI/Web/src/app/_services/localization.service.ts index 7519a9562..23ba213b6 100644 --- a/UI/Web/src/app/_services/localization.service.ts +++ b/UI/Web/src/app/_services/localization.service.ts @@ -1,36 +1,18 @@ -import {inject, Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; import {environment} from "../../environments/environment"; -import { HttpClient } from "@angular/common/http"; -import {KavitaLocale, Language} from "../_models/metadata/language"; -import {ReplaySubject, tap} from "rxjs"; -import {TranslocoService} from "@jsverse/transloco"; +import {HttpClient} from "@angular/common/http"; +import {Language} from "../_models/metadata/language"; @Injectable({ providedIn: 'root' }) export class LocalizationService { - private readonly translocoService = inject(TranslocoService); - baseUrl = environment.apiUrl; - private readonly localeSubject = new ReplaySubject(1); - public readonly locales$ = this.localeSubject.asObservable(); - constructor(private httpClient: HttpClient) { } getLocales() { - return this.httpClient.get(this.baseUrl + 'locale').pipe(tap(locales => { - this.localeSubject.next(locales); - })); - } - - refreshTranslations(lang: string) { - - // Clear the cached translation - localStorage.removeItem(`@@TRANSLOCO_PERSIST_TRANSLATIONS/${lang}`); - - // Reload the translation - return this.translocoService.load(lang); + return this.httpClient.get(this.baseUrl + 'locale'); } } diff --git a/UI/Web/src/app/_services/manage.service.ts b/UI/Web/src/app/_services/manage.service.ts deleted file mode 100644 index 781830caa..000000000 --- a/UI/Web/src/app/_services/manage.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; -import {ManageMatchSeries} from "../_models/kavitaplus/manage-match-series"; -import {ManageMatchFilter} from "../_models/kavitaplus/manage-match-filter"; - -@Injectable({ - providedIn: 'root' -}) -export class ManageService { - - baseUrl = environment.apiUrl; - private readonly httpClient = inject(HttpClient); - - getAllKavitaPlusSeries(filter: ManageMatchFilter) { - return this.httpClient.post>(this.baseUrl + `manage/series-metadata`, filter); - } -} diff --git a/UI/Web/src/app/_services/member.service.ts b/UI/Web/src/app/_services/member.service.ts index d93098995..27f21e6bc 100644 --- a/UI/Web/src/app/_services/member.service.ts +++ b/UI/Web/src/app/_services/member.service.ts @@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { Member } from '../_models/auth/member'; -import {UserTokenInfo} from "../_models/kavitaplus/user-token-info"; @Injectable({ providedIn: 'root' @@ -21,10 +20,6 @@ export class MemberService { return this.httpClient.get(this.baseUrl + 'users/names'); } - getUserTokenInfo() { - return this.httpClient.get(this.baseUrl + 'users/tokens'); - } - adminExists() { return this.httpClient.get(this.baseUrl + 'admin/exists'); } @@ -37,20 +32,20 @@ export class MemberService { return this.httpClient.get(this.baseUrl + 'users/has-library-access?libraryId=' + libraryId); } - hasReadingProgress(libraryId: number) { - return this.httpClient.get(this.baseUrl + 'users/has-reading-progress?libraryId=' + libraryId); + hasReadingProgress(librayId: number) { + return this.httpClient.get(this.baseUrl + 'users/has-reading-progress?libraryId=' + librayId); } addSeriesToWantToRead(seriesIds: Array) { - return this.httpClient.post(this.baseUrl + 'want-to-read/add-series', {seriesIds}); + return this.httpClient.post>(this.baseUrl + 'want-to-read/add-series', {seriesIds}); } removeSeriesToWantToRead(seriesIds: Array) { - return this.httpClient.post(this.baseUrl + 'want-to-read/remove-series', {seriesIds}); + return this.httpClient.post>(this.baseUrl + 'want-to-read/remove-series', {seriesIds}); } getMember() { return this.httpClient.get(this.baseUrl + 'users/myself'); } - + } diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index f870d1449..4eb17fb97 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -1,30 +1,23 @@ -import {Injectable} from '@angular/core'; -import {HubConnection, HubConnectionBuilder} from '@microsoft/signalr'; -import {BehaviorSubject, ReplaySubject} from 'rxjs'; -import {environment} from 'src/environments/environment'; -import {LibraryModifiedEvent} from '../_models/events/library-modified-event'; -import {NotificationProgressEvent} from '../_models/events/notification-progress-event'; -import {ThemeProgressEvent} from '../_models/events/theme-progress-event'; -import {UserUpdateEvent} from '../_models/events/user-update-event'; -import {User} from '../_models/user'; +import { Injectable } from '@angular/core'; +import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { LibraryModifiedEvent } from '../_models/events/library-modified-event'; +import { NotificationProgressEvent } from '../_models/events/notification-progress-event'; +import { ThemeProgressEvent } from '../_models/events/theme-progress-event'; +import { UserUpdateEvent } from '../_models/events/user-update-event'; +import { User } from '../_models/user'; import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event"; import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event"; -import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; -import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event"; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', ScanSeries = 'ScanSeries', SeriesAdded = 'SeriesAdded', SeriesRemoved = 'SeriesRemoved', - VolumeRemoved = 'VolumeRemoved', - ChapterRemoved = 'ChapterRemoved', ScanLibraryProgress = 'ScanLibraryProgress', OnlineUsers = 'OnlineUsers', - /** - * When a Collection has been updated - */ - CollectionUpdated = 'CollectionUpdated', + SeriesAddedToCollection = 'SeriesAddedToCollection', /** * A generic error that occurs during operations on the server */ @@ -47,10 +40,6 @@ export enum EVENTS { * A subtype of NotificationProgress that represents the underlying file being processed during a scan */ FileScanProgress = 'FileScanProgress', - /** - * A subtype of NotificationProgress that represents a single series being processed (into the DB) - */ - ScanProgress = 'ScanProgress', /** * A custom user site theme is added or removed during a scan */ @@ -102,23 +91,7 @@ export enum EVENTS { /** * User's sidenav needs to be re-rendered */ - SideNavUpdate = 'SideNavUpdate', - /** - * A Theme was updated and UI should refresh to get the latest version - */ - SiteThemeUpdated = 'SiteThemeUpdated', - /** - * A Progress event when a smart collection is synchronizing - */ - SmartCollectionSync = 'SmartCollectionSync', - /** - * A Person merged has been merged into another - */ - PersonMerged = 'PersonMerged', - /** - * A Rate limit error was hit when matching a series with Kavita+ - */ - ExternalMatchRateLimitError = 'ExternalMatchRateLimitError' + SideNavUpdate = 'SideNavUpdate' } export interface Message { @@ -168,7 +141,6 @@ export class MessageHubService { accessTokenFactory: () => user.token }) .withAutomaticReconnect() - //.withStatefulReconnect() // Requires signalr@8.0 .build(); this.hubConnection @@ -214,20 +186,6 @@ export class MessageHubService { }); }); - this.hubConnection.on(EVENTS.SmartCollectionSync, resp => { - this.messagesSource.next({ - event: EVENTS.NotificationProgress, - payload: resp.body - }); - }); - - this.hubConnection.on(EVENTS.SiteThemeUpdated, resp => { - this.messagesSource.next({ - event: EVENTS.SiteThemeUpdated, - payload: resp.body as SiteThemeUpdatedEvent - }); - }); - this.hubConnection.on(EVENTS.DashboardUpdate, resp => { this.messagesSource.next({ event: EVENTS.DashboardUpdate, @@ -241,13 +199,6 @@ export class MessageHubService { }); }); - this.hubConnection.on(EVENTS.ExternalMatchRateLimitError, resp => { - this.messagesSource.next({ - event: EVENTS.ExternalMatchRateLimitError, - payload: resp.body as ExternalMatchRateLimitErrorEvent - }); - }); - this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => { this.messagesSource.next({ event: EVENTS.NotificationProgress, @@ -262,9 +213,9 @@ export class MessageHubService { }); }); - this.hubConnection.on(EVENTS.CollectionUpdated, resp => { + this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => { this.messagesSource.next({ - event: EVENTS.CollectionUpdated, + event: EVENTS.SeriesAddedToCollection, payload: resp.body }); }); @@ -311,20 +262,6 @@ export class MessageHubService { }); }); - this.hubConnection.on(EVENTS.ChapterRemoved, resp => { - this.messagesSource.next({ - event: EVENTS.ChapterRemoved, - payload: resp.body - }); - }); - - this.hubConnection.on(EVENTS.VolumeRemoved, resp => { - this.messagesSource.next({ - event: EVENTS.VolumeRemoved, - payload: resp.body - }); - }); - this.hubConnection.on(EVENTS.CoverUpdate, resp => { this.messagesSource.next({ event: EVENTS.CoverUpdate, @@ -352,13 +289,6 @@ export class MessageHubService { payload: resp.body }); }); - - this.hubConnection.on(EVENTS.PersonMerged, resp => { - this.messagesSource.next({ - event: EVENTS.PersonMerged, - payload: resp.body - }); - }) } stopHubConnection() { diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index fe0702219..2e8214fb9 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -1,64 +1,33 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {inject, Injectable} from '@angular/core'; -import {tap} from 'rxjs/operators'; -import {map, of} from 'rxjs'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {map, tap} from 'rxjs/operators'; +import {of, ReplaySubject, switchMap} from 'rxjs'; import {environment} from 'src/environments/environment'; import {Genre} from '../_models/metadata/genre'; +import {AgeRating} from '../_models/metadata/age-rating'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {Language} from '../_models/metadata/language'; import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; -import {allPeopleRoles, Person, PersonRole} from '../_models/metadata/person'; +import {Person, PersonRole} from '../_models/metadata/person'; import {Tag} from '../_models/tag'; +import {TextResonse} from '../_types/text-response'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; import {FilterField} from '../_models/metadata/v2/filter-field'; -import {mangaFormatFilters, SortField} from "../_models/metadata/series-filter"; +import {Router} from "@angular/router"; +import {SortField} from "../_models/metadata/series-filter"; import {FilterCombination} from "../_models/metadata/v2/filter-combination"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {FilterStatement} from "../_models/metadata/v2/filter-statement"; -import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; -import {LibraryType} from "../_models/library/library"; -import {IHasCast} from "../_models/common/i-has-cast"; -import {TextResonse} from "../_types/text-response"; -import {QueryContext} from "../_models/metadata/v2/query-context"; -import {AgeRatingPipe} from "../_pipes/age-rating.pipe"; -import {MangaFormatPipe} from "../_pipes/manga-format.pipe"; -import {TranslocoService} from "@jsverse/transloco"; -import {LibraryService} from './library.service'; -import {CollectionTagService} from "./collection-tag.service"; -import {PaginatedResult} from "../_models/pagination"; -import {UtilityService} from "../shared/_services/utility.service"; -import {BrowseGenre} from "../_models/metadata/browse/browse-genre"; -import {BrowseTag} from "../_models/metadata/browse/browse-tag"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; -import {PersonRolePipe} from "../_pipes/person-role.pipe"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Injectable({ providedIn: 'root' }) export class MetadataService { - private readonly translocoService = inject(TranslocoService); - private readonly libraryService = inject(LibraryService); - private readonly collectionTagService = inject(CollectionTagService); - private readonly utilityService = inject(UtilityService); - baseUrl = environment.apiUrl; private validLanguages: Array = []; - private ageRatingPipe = new AgeRatingPipe(); - private mangaFormatPipe = new MangaFormatPipe(this.translocoService); - private personRolePipe = new PersonRolePipe(); - constructor(private httpClient: HttpClient) { } - - getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) { - return this.httpClient.get(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType); - } - - forceRefreshFromPlus(seriesId: number) { - return this.httpClient.post(this.baseUrl + 'metadata/force-refresh?seriesId=' + seriesId, {}); - } + constructor(private httpClient: HttpClient, private router: Router) { } getAllAgeRatings(libraries?: Array) { let method = 'metadata/age-ratings' @@ -84,39 +53,14 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } - getAllGenres(libraries?: Array, context: QueryContext = QueryContext.None) { + getAllGenres(libraries?: Array) { let method = 'metadata/genres' if (libraries != undefined && libraries.length > 0) { - method += '?libraryIds=' + libraries.join(',') + '&context=' + context; - } else { - method += '?context=' + context; + method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get>(this.baseUrl + method); } - getGenreWithCounts(pageNum?: number, itemsPerPage?: number) { - let params = new HttpParams(); - params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - - return this.httpClient.post>(this.baseUrl + 'metadata/genres-with-counts', {}, {observe: 'response', params}).pipe( - map((response: any) => { - return this.utilityService.createPaginatedResult(response) as PaginatedResult; - }) - ); - } - - getTagWithCounts(pageNum?: number, itemsPerPage?: number) { - let params = new HttpParams(); - params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - - return this.httpClient.post>(this.baseUrl + 'metadata/tags-with-counts', {}, {observe: 'response', params}).pipe( - map((response: any) => { - return this.utilityService.createPaginatedResult(response) as PaginatedResult; - }) - ); - } - getAllLanguages(libraries?: Array) { let method = 'metadata/languages' if (libraries != undefined && libraries.length > 0) { @@ -125,10 +69,6 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } - getLanguageNameForCode(code: string) { - return this.httpClient.get(`${this.baseUrl}metadata/language-title?code=${code}`, TextResonse); - } - /** * All the potential language tags there can be @@ -153,28 +93,23 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + 'metadata/people-by-role?role=' + role); } - createDefaultFilterDto(entityType: ValidFilterEntity): FilterV2 { + // getChapterSummary(chapterId: number) { + // return this.httpClient.get(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse); + // } + + createDefaultFilterDto(): SeriesFilterV2 { return { - statements: [] as FilterStatement[], + statements: [] as FilterStatement[], combination: FilterCombination.And, limitTo: 0, sortOptions: { isAscending: true, - sortField: (entityType === 'series' ? SortField.SortName : PersonSortField.Name) as TSort + sortField: SortField.SortName } }; } - createDefaultFilterStatement(entityType: ValidFilterEntity) { - switch (entityType) { - case 'series': - return this.createFilterStatement(FilterField.SeriesName); - case 'person': - return this.createFilterStatement(PersonFilterField.Role, FilterComparison.Contains, `${PersonRole.CoverArtist},${PersonRole.Writer}`); - } - } - - createFilterStatement(field: T, comparison = FilterComparison.Equal, value = '') { + createDefaultFilterStatement(field: FilterField = FilterField.SeriesName, comparison = FilterComparison.Equal, value = '') { return { comparison: comparison, field: field, @@ -182,136 +117,9 @@ export class MetadataService { }; } - updateFilter(arr: Array>, index: number, filterStmt: FilterStatement) { + updateFilter(arr: Array, index: number, filterStmt: FilterStatement) { arr[index].comparison = filterStmt.comparison; arr[index].field = filterStmt.field; arr[index].value = filterStmt.value ? filterStmt.value + '' : ''; } - - updatePerson(entity: IHasCast, persons: Person[], role: PersonRole) { - switch (role) { - case PersonRole.Other: - break; - case PersonRole.CoverArtist: - entity.coverArtists = persons; - break; - case PersonRole.Character: - entity.characters = persons; - break; - case PersonRole.Colorist: - entity.colorists = persons; - break; - case PersonRole.Editor: - entity.editors = persons; - break; - case PersonRole.Inker: - entity.inkers = persons; - break; - case PersonRole.Letterer: - entity.letterers = persons; - break; - case PersonRole.Penciller: - entity.pencillers = persons; - break; - case PersonRole.Publisher: - entity.publishers = persons; - break; - case PersonRole.Imprint: - entity.imprints = persons; - break; - case PersonRole.Team: - entity.teams = persons; - break; - case PersonRole.Location: - entity.locations = persons; - break; - case PersonRole.Writer: - entity.writers = persons; - break; - case PersonRole.Translator: - entity.translators = persons; - break; - } - } - - /** - * Used to get the underlying Options (for Metadata Filter Dropdowns) - * @param filterField - * @param entityType - */ - getOptionsForFilterField(filterField: T, entityType: ValidFilterEntity) { - - switch (entityType) { - case 'series': - return this.getSeriesOptionsForFilterField(filterField as FilterField); - case 'person': - return this.getPersonOptionsForFilterField(filterField as PersonFilterField); - } - } - - private getPersonOptionsForFilterField(field: PersonFilterField) { - switch (field) { - case PersonFilterField.Role: - return of(allPeopleRoles.map(r => {return {value: r, label: this.personRolePipe.transform(r)}})); - } - return of([]) - } - - private getSeriesOptionsForFilterField(field: FilterField) { - switch (field) { - case FilterField.PublicationStatus: - return this.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { - return {value: pub.value, label: pub.title} - }))); - case FilterField.AgeRating: - return this.getAllAgeRatings().pipe(map(ratings => ratings.map(rating => { - return {value: rating.value, label: this.ageRatingPipe.transform(rating.value)} - }))); - case FilterField.Genres: - return this.getAllGenres().pipe(map(genres => genres.map(genre => { - return {value: genre.id, label: genre.title} - }))); - case FilterField.Languages: - return this.getAllLanguages().pipe(map(statuses => statuses.map(status => { - return {value: status.isoCode, label: status.title + ` (${status.isoCode})`} - }))); - case FilterField.Formats: - return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => { - return {value: status.value, label: this.mangaFormatPipe.transform(status.value)} - }))); - case FilterField.Libraries: - return this.libraryService.getLibraries().pipe(map(libs => libs.map(lib => { - return {value: lib.id, label: lib.name} - }))); - case FilterField.Tags: - return this.getAllTags().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.CollectionTags: - return this.collectionTagService.allCollections().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.Characters: return this.getPersonOptions(PersonRole.Character); - case FilterField.Colorist: return this.getPersonOptions(PersonRole.Colorist); - case FilterField.CoverArtist: return this.getPersonOptions(PersonRole.CoverArtist); - case FilterField.Editor: return this.getPersonOptions(PersonRole.Editor); - case FilterField.Inker: return this.getPersonOptions(PersonRole.Inker); - case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer); - case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller); - case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher); - case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint); - case FilterField.Team: return this.getPersonOptions(PersonRole.Team); - case FilterField.Location: return this.getPersonOptions(PersonRole.Location); - case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator); - case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer); - } - - return of([]); - } - - private getPersonOptions(role: PersonRole) { - return this.getAllPeopleByRole(role).pipe(map(people => people.map(person => { - return {value: person.id, label: person.name} - }))); - } } diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 0aad76ef7..68df048f9 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -1,71 +1,18 @@ -import {DOCUMENT} from '@angular/common'; -import {DestroyRef, inject, Inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2} from '@angular/core'; -import {filter, ReplaySubject, take} from 'rxjs'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { ReplaySubject, take } from 'rxjs'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {TextResonse} from "../_types/text-response"; -import {AccountService} from "./account.service"; -import {map} from "rxjs/operators"; -import {NavigationEnd, Router} from "@angular/router"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component"; -import {WikiLink} from "../_models/wiki"; - -/** - * NavItem used to construct the dropdown or NavLinkModal on mobile - * Priority construction - * @param routerLink A link to a page on the web app, takes priority - * @param fragment Optional fragment for routerLink - * @param href A link to an external page, must set noopener noreferrer - * @param click Callback, lowest priority. Should only be used if routerLink and href or not set - */ -interface NavItem { - transLocoKey: string; - href?: string; - fragment?: string; - routerLink?: string; - click?: () => void; -} +import {DashboardStream} from "../_models/dashboard/dashboard-stream"; @Injectable({ providedIn: 'root' }) export class NavService { - - private readonly accountService = inject(AccountService); - private readonly router = inject(Router); - private readonly destroyRef = inject(DestroyRef); - public localStorageSideNavKey = 'kavita--sidenav--expanded'; - public navItems: NavItem[] = [ - { - transLocoKey: 'all-filters', - routerLink: '/all-filters/', - }, - { - transLocoKey: 'browse-genres', - routerLink: '/browse/genres', - }, - { - transLocoKey: 'browse-tags', - routerLink: '/browse/tags', - }, - { - transLocoKey: 'announcements', - routerLink: '/announcements/', - }, - { - transLocoKey: 'help', - href: WikiLink.Guides, - }, - { - transLocoKey: 'logout', - click: () => this.logout(), - } - ] - private navbarVisibleSource = new ReplaySubject(1); /** * If the top Nav bar is rendered or not @@ -84,31 +31,12 @@ export class NavService { */ sideNavVisibility$ = this.sideNavVisibilitySource.asObservable(); - usePreferenceSideNav$ = this.router.events.pipe( - filter(event => event instanceof NavigationEnd), - map((evt) => { - const event = (evt as NavigationEnd); - const url = event.urlAfterRedirects || event.url; - return ( - /\/admin\/dashboard(#.*)?/.test(url) || /\/preferences(\/[^\/]+|#.*)?/.test(url) || /\/settings(\/[^\/]+|#.*)?/.test(url) - ); - }), - takeUntilDestroyed(this.destroyRef), - ); - private renderer: Renderer2; baseUrl = environment.apiUrl; constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient) { this.renderer = rendererFactory.createRenderer(null, null); - - // To avoid flashing, let's check if we are authenticated before we show - this.accountService.currentUser$.pipe(take(1)).subscribe(u => { - if (u) { - this.showNavBar(); - } - }); - + this.showNavBar(); const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false; this.sideNavCollapseSource.next(sideNavState); this.showSideNav(); @@ -138,45 +66,24 @@ export class NavService { return this.httpClient.post(this.baseUrl + 'stream/bulk-sidenav-stream-visibility', {ids: streamIds, visibility: targetVisibility}); } - deleteSideNavSmartFilter(streamId: number) { - return this.httpClient.delete(this.baseUrl + 'stream/smart-filter-side-nav-stream?sideNavStreamId=' + streamId, {}); - } - /** * Shows the top nav bar. This should be visible on all pages except the reader. */ showNavBar() { - setTimeout(() => { - const bodyElem = this.document.querySelector('body'); - this.renderer.setStyle(bodyElem, 'margin-top', 'var(--nav-offset)'); - this.renderer.removeStyle(bodyElem, 'scrollbar-gutter'); - this.renderer.setStyle(bodyElem, 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); - this.renderer.setStyle(bodyElem, 'overflow', 'hidden'); - this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))'); - this.navbarVisibleSource.next(true); - }, 10); + this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '56px'); + this.renderer.setStyle(this.document.querySelector('body'), 'height', 'calc(var(--vh)*100 - 56px)'); + this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - 56px)'); + this.navbarVisibleSource.next(true); } /** * Hides the top nav bar. */ hideNavBar() { - setTimeout(() => { - const bodyElem = this.document.querySelector('body'); - this.renderer.removeStyle(bodyElem, 'height'); - this.renderer.setStyle(bodyElem, 'margin-top', '0px', RendererStyleFlags2.Important); - this.renderer.setStyle(bodyElem, 'scrollbar-gutter', 'initial', RendererStyleFlags2.Important); - this.renderer.removeStyle(this.document.querySelector('html'), 'height'); - this.renderer.setStyle(bodyElem, 'overflow', 'auto'); - this.navbarVisibleSource.next(false); - }, 10); - } - - logout() { - this.accountService.logout(); - this.hideNavBar(); - this.hideSideNav(); - this.router.navigateByUrl('/login'); + this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px'); + this.renderer.removeStyle(this.document.querySelector('body'), 'height'); + this.renderer.removeStyle(this.document.querySelector('html'), 'height'); + this.navbarVisibleSource.next(false); } /** @@ -201,9 +108,4 @@ export class NavService { localStorage.setItem(this.localStorageSideNavKey, newVal + ''); }); } - - collapseSideNav(isCollapsed: boolean) { - this.sideNavCollapseSource.next(isCollapsed); - localStorage.setItem(this.localStorageSideNavKey, isCollapsed + ''); - } } diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts deleted file mode 100644 index fc9148135..000000000 --- a/UI/Web/src/app/_services/person.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import {Injectable} from '@angular/core'; -import {HttpClient, HttpParams} from "@angular/common/http"; -import {environment} from "../../environments/environment"; -import {Person, PersonRole} from "../_models/metadata/person"; -import {PaginatedResult} from "../_models/pagination"; -import {Series} from "../_models/series"; -import {map} from "rxjs/operators"; -import {UtilityService} from "../shared/_services/utility.service"; -import {BrowsePerson} from "../_models/metadata/browse/browse-person"; -import {StandaloneChapter} from "../_models/standalone-chapter"; -import {TextResonse} from "../_types/text-response"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; - -@Injectable({ - providedIn: 'root' -}) -export class PersonService { - - baseUrl = environment.apiUrl; - - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - - updatePerson(person: Person) { - return this.httpClient.post(this.baseUrl + "person/update", person); - } - - get(name: string) { - return this.httpClient.get(this.baseUrl + `person?name=${name}`); - } - - searchPerson(name: string) { - return this.httpClient.get>(this.baseUrl + `person/search?queryString=${encodeURIComponent(name)}`); - } - - getRolesForPerson(personId: number) { - return this.httpClient.get>(this.baseUrl + `person/roles?personId=${personId}`); - } - - getSeriesMostKnownFor(personId: number) { - return this.httpClient.get>(this.baseUrl + `person/series-known-for?personId=${personId}`); - } - - getChaptersByRole(personId: number, role: PersonRole) { - return this.httpClient.get>(this.baseUrl + `person/chapters-by-role?personId=${personId}&role=${role}`); - } - - getAuthorsToBrowse(filter: FilterV2, pageNum?: number, itemsPerPage?: number) { - let params = new HttpParams(); - params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - - return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( - map((response: any) => { - return this.utilityService.createPaginatedResult(response) as PaginatedResult; - }) - ); - } - - // getAuthorsToBrowse(filter: BrowsePersonFilter, pageNum?: number, itemsPerPage?: number) { - // let params = new HttpParams(); - // params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - // - // return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( - // map((response: any) => { - // return this.utilityService.createPaginatedResult(response) as PaginatedResult; - // }) - // ); - // } - - downloadCover(personId: number) { - return this.httpClient.post(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse); - } - - isValidAlias(personId: number, alias: string) { - return this.httpClient.get(this.baseUrl + `person/valid-alias?personId=${personId}&alias=${alias}`, TextResonse).pipe( - map(valid => valid + '' === 'true') - ); - } - - mergePerson(destId: number, srcId: number) { - return this.httpClient.post(this.baseUrl + 'person/merge', {destId, srcId}); - } - -} diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 52aef2a4a..7c6547abf 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -1,30 +1,24 @@ -import {HttpClient} from '@angular/common/http'; -import {DestroyRef, Inject, inject, Injectable} from '@angular/core'; -import {DOCUMENT, Location} from '@angular/common'; -import {Router} from '@angular/router'; -import {environment} from 'src/environments/environment'; -import {ChapterInfo} from '../manga-reader/_models/chapter-info'; -import {Chapter} from '../_models/chapter'; -import {HourEstimateRange} from '../_models/series-detail/hour-estimate-range'; -import {MangaFormat} from '../_models/manga-format'; -import {BookmarkInfo} from '../_models/manga-reader/bookmark-info'; -import {PageBookmark} from '../_models/readers/page-bookmark'; -import {ProgressBookmark} from '../_models/readers/progress-bookmark'; -import {FileDimension} from '../manga-reader/_models/file-dimension'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import {DestroyRef, inject, Injectable} from '@angular/core'; +import {Location} from '@angular/common'; +import { Router } from '@angular/router'; +import { environment } from 'src/environments/environment'; +import { ChapterInfo } from '../manga-reader/_models/chapter-info'; +import { Chapter } from '../_models/chapter'; +import { HourEstimateRange } from '../_models/series-detail/hour-estimate-range'; +import { MangaFormat } from '../_models/manga-format'; +import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; +import { PageBookmark } from '../_models/readers/page-bookmark'; +import { ProgressBookmark } from '../_models/readers/progress-bookmark'; +import { UtilityService } from '../shared/_services/utility.service'; +import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service'; +import { FileDimension } from '../manga-reader/_models/file-dimension'; import screenfull from 'screenfull'; -import {TextResonse} from '../_types/text-response'; -import {AccountService} from './account.service'; +import { TextResonse } from '../_types/text-response'; +import { AccountService } from './account.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {PersonalToC} from "../_models/readers/personal-toc"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; -import NoSleep from 'nosleep.js'; -import {FullProgress} from "../_models/readers/full-progress"; -import {Volume} from "../_models/volume"; -import {UtilityService} from "../shared/_services/utility.service"; -import {translate} from "@jsverse/transloco"; -import {ToastrService} from "ngx-toastr"; -import {FilterField} from "../_models/metadata/v2/filter-field"; - +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; export const CHAPTER_ID_DOESNT_EXIST = -1; export const CHAPTER_ID_NOT_FETCHED = -2; @@ -35,22 +29,15 @@ export const CHAPTER_ID_NOT_FETCHED = -2; export class ReaderService { private readonly destroyRef = inject(DestroyRef); - private readonly utilityService = inject(UtilityService); - private readonly router = inject(Router); - private readonly location = inject(Location); - private readonly accountService = inject(AccountService); - private readonly toastr = inject(ToastrService); - baseUrl = environment.apiUrl; encodedKey: string = ''; // Override background color for reader and restore it onDestroy private originalBodyColor!: string; - - private noSleep: NoSleep = new NoSleep(); - - constructor(private httpClient: HttpClient, @Inject(DOCUMENT) private document: Document) { + constructor(private httpClient: HttpClient, private router: Router, + private location: Location, private utilityService: UtilityService, + private filterUtilityService: FilterUtilitiesService, private accountService: AccountService) { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { if (user) { this.encodedKey = encodeURIComponent(user.apiKey); @@ -59,31 +46,6 @@ export class ReaderService { } - enableWakeLock(element?: Element | Document) { - // Enable wake lock. - // (must be wrapped in a user input event handler e.g. a mouse or touch handler) - - if (!element) element = this.document; - - const enableNoSleepHandler = async () => { - element!.removeEventListener('click', enableNoSleepHandler, false); - element!.removeEventListener('touchmove', enableNoSleepHandler, false); - element!.removeEventListener('mousemove', enableNoSleepHandler, false); - await this.noSleep.enable(); - }; - - // Enable wake lock. - // (must be wrapped in a user input event handler e.g. a mouse or touch handler) - element.addEventListener('click', enableNoSleepHandler, false); - element.addEventListener('touchmove', enableNoSleepHandler, false); - element.addEventListener('mousemove', enableNoSleepHandler, false); - } - - disableWakeLock() { - this.noSleep.disable(); - } - - getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { if (format === undefined) format = MangaFormat.ARCHIVE; @@ -108,7 +70,7 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page}); } - getAllBookmarks(filter: FilterV2 | undefined) { + getAllBookmarks(filter: SeriesFilterV2 | undefined) { return this.httpClient.post(this.baseUrl + 'reader/all-bookmarks', filter); } @@ -167,10 +129,6 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/progress', {libraryId, seriesId, volumeId, chapterId, pageNum: page, bookScrollId}); } - getAllProgressForChapter(chapterId: number) { - return this.httpClient.get>(this.baseUrl + 'reader/all-chapter-progress?chapterId=' + chapterId); - } - markVolumeRead(seriesId: number, volumeId: number) { return this.httpClient.post(this.baseUrl + 'reader/mark-volume-read', {seriesId, volumeId}); } @@ -266,13 +224,13 @@ export class ReaderService { getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { - const params: {[key: string]: any} = {}; - params['incognitoMode'] = incognitoMode; - + let params: {[key: string]: any} = {}; + if (incognitoMode) { + params['incognitoMode'] = true; + } if (readingListMode) { params['readingListId'] = readingListId; } - return params; } @@ -313,7 +271,6 @@ export class ReaderService { if (readingListMode) { this.router.navigateByUrl('lists/' + readingListId); } else { - // TODO: back doesn't always work, it might be nice to check the pattern of the url and see if we can be smart before just going back this.location.back(); } } @@ -364,30 +321,4 @@ export class ReaderService { } return ''; } - - readVolume(libraryId: number, seriesId: number, volume: Volume, incognitoMode: boolean = false) { - if (volume.pagesRead < volume.pages && volume.pagesRead > 0) { - // Find the continue point chapter and load it - const unreadChapters = volume.chapters.filter(item => item.pagesRead < item.pages); - if (unreadChapters.length > 0) { - this.readChapter(libraryId, seriesId, unreadChapters[0], incognitoMode); - return; - } - this.readChapter(libraryId, seriesId, volume.chapters[0], incognitoMode); - return; - } - - // Sort the chapters, then grab first if no reading progress - this.readChapter(libraryId, seriesId, [...volume.chapters].sort(this.utilityService.sortChapters)[0], incognitoMode); - } - - readChapter(libraryId: number, seriesId: number, chapter: Chapter, incognitoMode: boolean = false) { - if (chapter.pages === 0) { - this.toastr.error(translate('series-detail.no-pages')); - return; - } - - this.router.navigate(this.getNavigationArray(libraryId, seriesId, chapter.id, chapter.files[0].format), - {queryParams: {incognitoMode}}); - } } diff --git a/UI/Web/src/app/_services/reading-list.service.ts b/UI/Web/src/app/_services/reading-list.service.ts index 088263a33..4789fe67d 100644 --- a/UI/Web/src/app/_services/reading-list.service.ts +++ b/UI/Web/src/app/_services/reading-list.service.ts @@ -1,14 +1,14 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {map} from 'rxjs/operators'; -import {environment} from 'src/environments/environment'; -import {UtilityService} from '../shared/_services/utility.service'; -import {Person, PersonRole} from '../_models/metadata/person'; -import {PaginatedResult} from '../_models/pagination'; -import {ReadingList, ReadingListCast, ReadingListInfo, ReadingListItem} from '../_models/reading-list'; -import {CblImportSummary} from '../_models/reading-list/cbl/cbl-import-summary'; -import {TextResonse} from '../_types/text-response'; -import {Action, ActionItem} from './action-factory.service'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { UtilityService } from '../shared/_services/utility.service'; +import { Person } from '../_models/metadata/person'; +import { PaginatedResult } from '../_models/pagination'; +import { ReadingList, ReadingListItem } from '../_models/reading-list'; +import { CblImportSummary } from '../_models/reading-list/cbl/cbl-import-summary'; +import { TextResonse } from '../_types/text-response'; +import { ActionItem } from './action-factory.service'; @Injectable({ providedIn: 'root' @@ -20,7 +20,7 @@ export class ReadingListService { constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } getReadingList(readingListId: number) { - return this.httpClient.get(this.baseUrl + 'readinglist?readingListId=' + readingListId); + return this.httpClient.get(this.baseUrl + 'readinglist?readingListId=' + readingListId); } getReadingLists(includePromoted: boolean = true, sortByLastModified: boolean = false, pageNum?: number, itemsPerPage?: number) { @@ -39,10 +39,6 @@ export class ReadingListService { return this.httpClient.get(this.baseUrl + 'readinglist/lists-for-series?seriesId=' + seriesId); } - getReadingListsForChapter(chapterId: number) { - return this.httpClient.get(this.baseUrl + 'readinglist/lists-for-chapter?chapterId=' + chapterId); - } - getListItems(readingListId: number) { return this.httpClient.get(this.baseUrl + 'readinglist/items?readingListId=' + readingListId); } @@ -91,50 +87,24 @@ export class ReadingListService { return this.httpClient.post(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, {}, TextResonse); } - actionListFilter(action: ActionItem, readingList: ReadingList, canPromote: boolean) { - - const isPromotionAction = action.action == Action.Promote || action.action == Action.UnPromote; - - if (isPromotionAction) return canPromote; + actionListFilter(action: ActionItem, readingList: ReadingList, isAdmin: boolean) { + if (readingList?.promoted && !isAdmin) return false; return true; - - // if (readingList?.promoted && !isAdmin) return false; - // return true; } nameExists(name: string) { return this.httpClient.get(this.baseUrl + 'readinglist/name-exists?name=' + name); } - validateCbl(form: FormData, dryRun: boolean, useComicVineMatching: boolean) { - return this.httpClient.post(this.baseUrl + `cbl/validate?dryRun=${dryRun}&useComicVineMatching=${useComicVineMatching}`, form); + validateCbl(form: FormData) { + return this.httpClient.post(this.baseUrl + 'cbl/validate', form); } - importCbl(form: FormData, dryRun: boolean, useComicVineMatching: boolean) { - return this.httpClient.post(this.baseUrl + `cbl/import?dryRun=${dryRun}&useComicVineMatching=${useComicVineMatching}`, form); + importCbl(form: FormData) { + return this.httpClient.post(this.baseUrl + 'cbl/import', form); } - getPeople(readingListId: number, role: PersonRole) { - return this.httpClient.get>(this.baseUrl + `readinglist/people?readingListId=${readingListId}&role=${role}`); + getCharacters(readingListId: number) { + return this.httpClient.get>(this.baseUrl + 'readinglist/characters?readingListId=' + readingListId); } - - getAllPeople(readingListId: number) { - return this.httpClient.get(this.baseUrl + `readinglist/all-people?readingListId=${readingListId}`); - } - - - getReadingListInfo(readingListId: number) { - return this.httpClient.get(this.baseUrl + `readinglist/info?readingListId=${readingListId}`); - } - - - promoteMultipleReadingLists(listIds: Array, promoted: boolean) { - return this.httpClient.post(this.baseUrl + 'readinglist/promote-multiple', {readingListIds: listIds, promoted}, TextResonse); - } - - deleteMultipleReadingLists(listIds: Array) { - return this.httpClient.post(this.baseUrl + 'readinglist/delete-multiple', {readingListIds: listIds}, TextResonse); - } - - } diff --git a/UI/Web/src/app/_services/reading-profile.service.ts b/UI/Web/src/app/_services/reading-profile.service.ts deleted file mode 100644 index e8be8b6ab..000000000 --- a/UI/Web/src/app/_services/reading-profile.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {HttpClient} from "@angular/common/http"; -import {environment} from "../../environments/environment"; -import {ReadingProfile} from "../_models/preferences/reading-profiles"; - -@Injectable({ - providedIn: 'root' -}) -export class ReadingProfileService { - - private readonly httpClient = inject(HttpClient); - baseUrl = environment.apiUrl; - - getForSeries(seriesId: number, skipImplicit: boolean = false) { - return this.httpClient.get(this.baseUrl + `reading-profile/${seriesId}?skipImplicit=${skipImplicit}`); - } - - getForLibrary(libraryId: number) { - return this.httpClient.get(this.baseUrl + `reading-profile/library?libraryId=${libraryId}`); - } - - updateProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + 'reading-profile', profile); - } - - updateParentProfile(seriesId: number, profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + `reading-profile/update-parent?seriesId=${seriesId}`, profile); - } - - createProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + 'reading-profile/create', profile); - } - - promoteProfile(profileId: number) { - return this.httpClient.post(this.baseUrl + "reading-profile/promote?profileId=" + profileId, {}); - } - - updateImplicit(profile: ReadingProfile, seriesId: number) { - return this.httpClient.post(this.baseUrl + "reading-profile/series?seriesId="+seriesId, profile); - } - - getAllProfiles() { - return this.httpClient.get(this.baseUrl + 'reading-profile/all'); - } - - delete(id: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile?profileId=${id}`); - } - - addToSeries(id: number, seriesId: number) { - return this.httpClient.post(this.baseUrl + `reading-profile/series/${seriesId}?profileId=${id}`, {}); - } - - clearSeriesProfiles(seriesId: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile/series/${seriesId}`, {}); - } - - addToLibrary(id: number, libraryId: number) { - return this.httpClient.post(this.baseUrl + `reading-profile/library/${libraryId}?profileId=${id}`, {}); - } - - clearLibraryProfiles(libraryId: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile/library/${libraryId}`, {}); - } - - bulkAddToSeries(id: number, seriesIds: number[]) { - return this.httpClient.post(this.baseUrl + `reading-profile/bulk?profileId=${id}`, seriesIds); - } - -} diff --git a/UI/Web/src/app/_services/review.service.ts b/UI/Web/src/app/_services/review.service.ts deleted file mode 100644 index b8635bcf8..000000000 --- a/UI/Web/src/app/_services/review.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from '@angular/core'; -import {UserReview} from "../_single-module/review-card/user-review"; -import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; -import {Rating} from "../_models/rating"; - -@Injectable({ - providedIn: 'root' -}) -export class ReviewService { - - private baseUrl = environment.apiUrl; - - constructor(private httpClient: HttpClient) { } - - deleteReview(seriesId: number, chapterId?: number) { - if (chapterId) { - return this.httpClient.delete(this.baseUrl + `review/chapter?chapterId=${chapterId}`); - } - - return this.httpClient.delete(this.baseUrl + `review/series?seriesId=${seriesId}`); - } - - updateReview(seriesId: number, body: string, chapterId?: number) { - if (chapterId) { - return this.httpClient.post(this.baseUrl + `review/chapter`, { - seriesId, chapterId, body - }); - } - - return this.httpClient.post(this.baseUrl + 'review/series', { - seriesId, body - }); - } - - updateRating(seriesId: number, userRating: number, chapterId?: number) { - if (chapterId) { - return this.httpClient.post(this.baseUrl + 'rating/chapter', { - seriesId, chapterId, userRating - }) - } - - return this.httpClient.post(this.baseUrl + 'rating/series', { - seriesId, userRating - }) - } - - overallRating(seriesId: number, chapterId?: number) { - if (chapterId) { - return this.httpClient.get(this.baseUrl + `rating/overall-chapter?chapterId=${chapterId}`); - } - - return this.httpClient.get(this.baseUrl + `rating/overall-series?seriesId=${seriesId}`); - } - -} diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index cfc7b34ac..9edca977c 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -1,8 +1,8 @@ import {HttpClient, HttpParams} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {map} from 'rxjs/operators'; -import {environment} from 'src/environments/environment'; -import {TextResonse} from '../_types/text-response'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { TextResonse } from '../_types/text-response'; import {ScrobbleError} from "../_models/scrobbling/scrobble-error"; import {ScrobbleEvent} from "../_models/scrobbling/scrobble-event"; import {ScrobbleHold} from "../_models/scrobbling/scrobble-hold"; @@ -12,10 +12,9 @@ import {UtilityService} from "../shared/_services/utility.service"; export enum ScrobbleProvider { Kavita = 0, - AniList = 1, + AniList= 1, Mal = 2, - GoogleBooks = 3, - Cbr = 4 + GoogleBooks = 3 } @Injectable({ @@ -33,35 +32,14 @@ export class ScrobblingService { .pipe(map(r => r === "true")); } - /** - * Returns if the token was new or not - */ updateAniListToken(token: string) { - return this.httpClient.post(this.baseUrl + 'scrobbling/update-anilist-token', {token}, TextResonse) - .pipe(map(r => r + '' === 'true')); - } - - /** - * Returns if the token was new or not - */ - updateMalToken(username: string, accessToken: string) { - return this.httpClient.post(this.baseUrl + 'scrobbling/update-mal-token', {username, accessToken}, TextResonse) - .pipe(map(r => r + '' === 'true')); + return this.httpClient.post(this.baseUrl + 'scrobbling/update-anilist-token', {token}); } getAniListToken() { return this.httpClient.get(this.baseUrl + 'scrobbling/anilist-token', TextResonse); } - getMalToken() { - return this.httpClient.get<{username: string, accessToken: string}>(this.baseUrl + 'scrobbling/mal-token'); - } - - - hasRunScrobbleGen() { - return this.httpClient.get(this.baseUrl + 'scrobbling/has-ran-scrobble-gen ', TextResonse).pipe(map(r => r === 'true')); - } - getScrobbleErrors() { return this.httpClient.get>(this.baseUrl + 'scrobbling/scrobble-errors'); } @@ -101,13 +79,4 @@ export class ScrobblingService { removeHold(seriesId: number) { return this.httpClient.delete(this.baseUrl + 'scrobbling/remove-hold?seriesId=' + seriesId, TextResonse); } - - triggerScrobbleEventGeneration() { - return this.httpClient.post(this.baseUrl + 'scrobbling/generate-scrobble-events', TextResonse); - } - - bulkRemoveEvents(eventIds: number[]) { - return this.httpClient.post(this.baseUrl + "scrobbling/bulk-remove-events", eventIds) - } - } diff --git a/UI/Web/src/app/_services/search.service.ts b/UI/Web/src/app/_services/search.service.ts index 4a95fff99..fa989fa35 100644 --- a/UI/Web/src/app/_services/search.service.ts +++ b/UI/Web/src/app/_services/search.service.ts @@ -14,11 +14,11 @@ export class SearchService { constructor(private httpClient: HttpClient) { } - search(term: string, includeChapterAndFiles: boolean = false) { + search(term: string) { if (term === '') { return of(new SearchResultGroup()); } - return this.httpClient.get(this.baseUrl + `search/search?includeChapterAndFiles=${includeChapterAndFiles}&queryString=${encodeURIComponent(term)}`); + return this.httpClient.get(this.baseUrl + 'search/search?queryString=' + encodeURIComponent(term)); } getSeriesForMangaFile(mangaFileId: number) { diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 9c436e636..6a92a491e 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -1,26 +1,27 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; -import {environment} from 'src/environments/environment'; -import {UtilityService} from '../shared/_services/utility.service'; -import {Chapter} from '../_models/chapter'; -import {PaginatedResult} from '../_models/pagination'; -import {Series} from '../_models/series'; -import {RelatedSeries} from '../_models/series-detail/related-series'; -import {SeriesDetail} from '../_models/series-detail/series-detail'; -import {SeriesGroup} from '../_models/series-group'; -import {SeriesMetadata} from '../_models/metadata/series-metadata'; -import {Volume} from '../_models/volume'; -import {TextResonse} from '../_types/text-response'; -import {FilterV2} from '../_models/metadata/v2/filter-v2'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { UtilityService } from '../shared/_services/utility.service'; +import { Chapter } from '../_models/chapter'; +import { ChapterMetadata } from '../_models/metadata/chapter-metadata'; +import { CollectionTag } from '../_models/collection-tag'; +import { PaginatedResult } from '../_models/pagination'; +import { Series } from '../_models/series'; +import { RelatedSeries } from '../_models/series-detail/related-series'; +import { SeriesDetail } from '../_models/series-detail/series-detail'; +import { SeriesGroup } from '../_models/series-group'; +import { SeriesMetadata } from '../_models/metadata/series-metadata'; +import { Volume } from '../_models/volume'; +import { ImageService } from './image.service'; +import { TextResonse } from '../_types/text-response'; +import { SeriesFilterV2 } from '../_models/metadata/v2/series-filter-v2'; +import {UserReview} from "../_single-module/review-card/user-review"; import {Rating} from "../_models/rating"; import {Recommendation} from "../_models/series-detail/recommendation"; import {ExternalSeriesDetail} from "../_models/series-detail/external-series-detail"; import {NextExpectedChapter} from "../_models/series-detail/next-expected-chapter"; -import {QueryContext} from "../_models/metadata/v2/query-context"; -import {ExternalSeriesMatch} from "../_models/series-detail/external-series-match"; -import {FilterField} from "../_models/metadata/v2/filter-field"; @Injectable({ providedIn: 'root' @@ -31,21 +32,22 @@ export class SeriesService { paginatedResults: PaginatedResult = new PaginatedResult(); paginatedSeriesForTagsResults: PaginatedResult = new PaginatedResult(); - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } + constructor(private httpClient: HttpClient, private imageService: ImageService, + private utilityService: UtilityService) { } - getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2, context: QueryContext = QueryContext.None) { + getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; - return this.httpClient.post>(this.baseUrl + 'series/all-v2?context=' + context, data, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + 'series/all-v2', data, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response, this.paginatedResults); }) ); } - getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -73,12 +75,20 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/chapter?chapterId=' + chapterId); } + getChapterMetadata(chapterId: number) { + return this.httpClient.get(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId); + } + delete(seriesId: number) { - return this.httpClient.delete(this.baseUrl + 'series/' + seriesId, TextResonse).pipe(map(s => s === "true")); + return this.httpClient.delete(this.baseUrl + 'series/' + seriesId); } deleteMultipleSeries(seriesIds: Array) { - return this.httpClient.post(this.baseUrl + 'series/delete-multiple', {seriesIds}, TextResonse).pipe(map(s => s === "true")); + return this.httpClient.post(this.baseUrl + 'series/delete-multiple', {seriesIds}); + } + + updateRating(seriesId: number, userRating: number) { + return this.httpClient.post(this.baseUrl + 'series/update-rating', {seriesId, userRating}); } updateSeries(model: any) { @@ -93,7 +103,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'reader/mark-unread', {seriesId}); } - getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); @@ -109,7 +119,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'series/recently-updated-series', {}); } - getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: FilterV2): Observable> { + getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2): Observable> { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -127,7 +137,7 @@ export class SeriesService { })); } - getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -139,8 +149,8 @@ export class SeriesService { } - refreshMetadata(series: Series, force = true, forceColorscape = true) { - return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id, forceUpdate: force, forceColorscape}); + refreshMetadata(series: Series) { + return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id}); } scan(libraryId: number, seriesId: number, force = false) { @@ -152,12 +162,16 @@ export class SeriesService { } getMetadata(seriesId: number) { - return this.httpClient.get(this.baseUrl + 'series/metadata?seriesId=' + seriesId); + return this.httpClient.get(this.baseUrl + 'series/metadata?seriesId=' + seriesId).pipe(map(items => { + items?.collectionTags.forEach(tag => tag.coverImage = this.imageService.getCollectionCoverImage(tag.id)); + return items; + })); } - updateMetadata(seriesMetadata: SeriesMetadata) { + updateMetadata(seriesMetadata: SeriesMetadata, collectionTags: CollectionTag[]) { const data = { seriesMetadata, + collectionTags, }; return this.httpClient.post(this.baseUrl + 'series/metadata', data, TextResonse); } @@ -185,20 +199,32 @@ export class SeriesService { updateRelationships(seriesId: number, adaptations: Array, characters: Array, contains: Array, others: Array, prequels: Array, sequels: Array, sideStories: Array, spinOffs: Array, - alternativeSettings: Array, alternativeVersions: Array, - doujinshis: Array, editions: Array, annuals: Array) { + alternativeSettings: Array, alternativeVersions: Array, doujinshis: Array, editions: Array) { return this.httpClient.post(this.baseUrl + 'series/update-related?seriesId=' + seriesId, {seriesId, adaptations, characters, sequels, prequels, contains, others, sideStories, spinOffs, - alternativeSettings, alternativeVersions, doujinshis, editions, annuals}); + alternativeSettings, alternativeVersions, doujinshis, editions}); } getSeriesDetail(seriesId: number) { return this.httpClient.get(this.baseUrl + 'series/series-detail?seriesId=' + seriesId); } + getReviews(seriesId: number) { + return this.httpClient.get>(this.baseUrl + 'review?seriesId=' + seriesId); + } + + updateReview(seriesId: number, tagline: string, body: string) { + return this.httpClient.post(this.baseUrl + 'review', { + seriesId, tagline, body + }); + } + getRatings(seriesId: number) { return this.httpClient.get>(this.baseUrl + 'rating?seriesId=' + seriesId); } + getOverallRating(seriesId: number) { + return this.httpClient.get(this.baseUrl + 'rating/overall?seriesId=' + seriesId); + } removeFromOnDeck(seriesId: number) { return this.httpClient.post(this.baseUrl + 'series/remove-from-on-deck?seriesId=' + seriesId, {}); @@ -212,15 +238,4 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/next-expected?seriesId=' + seriesId); } - matchSeries(model: any) { - return this.httpClient.post>(this.baseUrl + 'series/match', model); - } - - updateMatch(seriesId: number, series: ExternalSeriesDetail) { - return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId || 0}&malId=${series.malId || 0}&cbrId=${series.cbrId || 0}`, {}, TextResonse); - } - - updateDontMatch(seriesId: number, dontMatch: boolean) { - return this.httpClient.post(this.baseUrl + `series/dont-match?seriesId=${seriesId}&dontMatch=${dontMatch}`, {}, TextResonse); - } } diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 4a71e836e..3f3a88a25 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -6,7 +6,6 @@ import { UpdateVersionEvent } from '../_models/events/update-version-event'; import { Job } from '../_models/job/job'; import { KavitaMediaError } from '../admin/_models/media-error'; import {TextResonse} from "../_types/text-response"; -import {map} from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -15,66 +14,62 @@ export class ServerService { baseUrl = environment.apiUrl; - constructor(private http: HttpClient) { } + constructor(private httpClient: HttpClient) { } - getVersion(apiKey: string) { - return this.http.get(this.baseUrl + 'plugin/version?apiKey=' + apiKey, TextResonse); - } getServerInfo() { - return this.http.get(this.baseUrl + 'server/server-info-slim'); + return this.httpClient.get(this.baseUrl + 'server/server-info-slim'); } clearCache() { - return this.http.post(this.baseUrl + 'server/clear-cache', {}); + return this.httpClient.post(this.baseUrl + 'server/clear-cache', {}); } cleanupWantToRead() { - return this.http.post(this.baseUrl + 'server/cleanup-want-to-read', {}); - } - - cleanup() { - return this.http.post(this.baseUrl + 'server/cleanup', {}); + return this.httpClient.post(this.baseUrl + 'server/cleanup-want-to-read', {}); } backupDatabase() { - return this.http.post(this.baseUrl + 'server/backup-db', {}); + return this.httpClient.post(this.baseUrl + 'server/backup-db', {}); } - syncThemes() { - return this.http.post(this.baseUrl + 'server/sync-themes', {}); + analyzeFiles() { + return this.httpClient.post(this.baseUrl + 'server/analyze-files', {}); } checkForUpdate() { - return this.http.get(this.baseUrl + 'server/check-update'); + return this.httpClient.get(this.baseUrl + 'server/check-update', {}); } - checkHowOutOfDate(stableOnly: boolean = true) { - return this.http.get(this.baseUrl + `server/check-out-of-date?stableOnly=${stableOnly}`, TextResonse) - .pipe(map(r => parseInt(r, 10))); + getChangelog() { + return this.httpClient.get(this.baseUrl + 'server/changelog', {}); } - getChangelog(count: number = 0) { - return this.http.get(this.baseUrl + 'server/changelog?count=' + count, {}); + isServerAccessible() { + return this.httpClient.get(this.baseUrl + 'server/accessible'); } getRecurringJobs() { - return this.http.get(this.baseUrl + 'server/jobs'); + return this.httpClient.get(this.baseUrl + 'server/jobs'); } convertMedia() { - return this.http.post(this.baseUrl + 'server/convert-media', {}); + return this.httpClient.post(this.baseUrl + 'server/convert-media', {}); } bustCache() { - return this.http.post(this.baseUrl + 'server/bust-kavitaplus-cache', {}); + return this.httpClient.post(this.baseUrl + 'server/bust-review-and-rec-cache', {}); } getMediaErrors() { - return this.http.get>(this.baseUrl + 'server/media-errors', {}); + return this.httpClient.get>(this.baseUrl + 'server/media-errors', {}); } clearMediaAlerts() { - return this.http.post(this.baseUrl + 'server/clear-media-alerts', {}); + return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {}); + } + + getEmailVersion() { + return this.httpClient.get(this.baseUrl + 'server/email-version', TextResonse); } } diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index cf80765f2..ff87fa2c4 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -1,23 +1,19 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {Inject, inject, Injectable} from '@angular/core'; -import {environment} from 'src/environments/environment'; -import {UserReadStatistics} from '../statistics/_models/user-read-statistics'; -import {PublicationStatusPipe} from '../_pipes/publication-status.pipe'; -import {asyncScheduler, map} from 'rxjs'; -import {MangaFormatPipe} from '../_pipes/manga-format.pipe'; -import {FileExtensionBreakdown} from '../statistics/_models/file-breakdown'; -import {TopUserRead} from '../statistics/_models/top-reads'; -import {ReadHistoryEvent} from '../statistics/_models/read-history-event'; -import {ServerStatistics} from '../statistics/_models/server-statistics'; -import {StatCount} from '../statistics/_models/stat-count'; -import {PublicationStatus} from '../_models/metadata/publication-status'; -import {MangaFormat} from '../_models/manga-format'; -import {TextResonse} from '../_types/text-response'; -import {TranslocoService} from "@jsverse/transloco"; -import {throttleTime} from "rxjs/operators"; -import {DEBOUNCE_TIME} from "../shared/_services/download.service"; -import {download} from "../shared/_models/download"; -import {Saver, SAVER} from "../_providers/saver.provider"; +import { HttpClient } from '@angular/common/http'; +import {inject, Injectable} from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { UserReadStatistics } from '../statistics/_models/user-read-statistics'; +import { PublicationStatusPipe } from '../pipe/publication-status.pipe'; +import { map } from 'rxjs'; +import { MangaFormatPipe } from '../pipe/manga-format.pipe'; +import { FileExtensionBreakdown } from '../statistics/_models/file-breakdown'; +import { TopUserRead } from '../statistics/_models/top-reads'; +import { ReadHistoryEvent } from '../statistics/_models/read-history-event'; +import { ServerStatistics } from '../statistics/_models/server-statistics'; +import { StatCount } from '../statistics/_models/stat-count'; +import { PublicationStatus } from '../_models/metadata/publication-status'; +import { MangaFormat } from '../_models/manga-format'; +import { TextResonse } from '../_types/text-response'; +import {TranslocoService} from "@ngneat/transloco"; export enum DayOfWeek { @@ -40,17 +36,14 @@ export class StatisticsService { publicationStatusPipe = new PublicationStatusPipe(this.translocoService); mangaFormatPipe = new MangaFormatPipe(this.translocoService); - constructor(private httpClient: HttpClient, @Inject(SAVER) private save: Saver) { } + constructor(private httpClient: HttpClient) { } getUserStatistics(userId: number, libraryIds: Array = []) { - const url = `${this.baseUrl}stats/user/${userId}/read`; + // TODO: Convert to httpParams object + let url = 'stats/user/' + userId + '/read'; + if (libraryIds.length > 0) url += '?libraryIds=' + libraryIds.join(','); - let params = new HttpParams(); - if (libraryIds.length > 0) { - params = params.set('libraryIds', libraryIds.join(',')); - } - - return this.httpClient.get(url, { params }); + return this.httpClient.get(this.baseUrl + url); } getServerStatistics() { @@ -61,7 +54,7 @@ export class StatisticsService { return this.httpClient.get[]>(this.baseUrl + 'stats/server/count/year').pipe( map(spreads => spreads.map(spread => { return {name: spread.value + '', value: spread.count}; - }))); + }))); } getTopYears() { @@ -115,20 +108,6 @@ export class StatisticsService { return this.httpClient.get(this.baseUrl + 'stats/server/file-breakdown'); } - downloadFileBreakdown(extension: string) { - return this.httpClient.get(this.baseUrl + 'stats/server/file-extension?fileExtension=' + encodeURIComponent(extension), - {observe: 'events', responseType: 'blob', reportProgress: true} - ).pipe( - throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), - download((blob, filename) => { - this.save(blob, decodeURIComponent(filename)); - }), - // tap((d) => this.updateDownloadState(d, downloadType, subtitle, 0)), - // finalize(() => this.finalizeDownloadState(downloadType, subtitle)) - ); - - } - getReadCountByDay(userId: number = 0, days: number = 0) { return this.httpClient.get>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days); } diff --git a/UI/Web/src/app/_services/theme.service.ts b/UI/Web/src/app/_services/theme.service.ts index 3e186f8ac..5b7e325b9 100644 --- a/UI/Web/src/app/_services/theme.service.ts +++ b/UI/Web/src/app/_services/theme.service.ts @@ -1,4 +1,4 @@ -import {DOCUMENT} from '@angular/common'; +import { DOCUMENT } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { DestroyRef, @@ -9,24 +9,18 @@ import { RendererFactory2, SecurityContext } from '@angular/core'; -import {DomSanitizer} from '@angular/platform-browser'; -import {ToastrService} from 'ngx-toastr'; -import {filter, map, ReplaySubject, take, tap} from 'rxjs'; -import {environment} from 'src/environments/environment'; -import {ConfirmService} from '../shared/confirm.service'; -import {NotificationProgressEvent} from '../_models/events/notification-progress-event'; -import {SiteTheme, ThemeProvider} from '../_models/preferences/site-theme'; -import {TextResonse} from '../_types/text-response'; -import {EVENTS, MessageHubService} from './message-hub.service'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ToastrService } from 'ngx-toastr'; +import { map, ReplaySubject, take } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { ConfirmService } from '../shared/confirm.service'; +import { NotificationProgressEvent } from '../_models/events/notification-progress-event'; +import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme'; +import { TextResonse } from '../_types/text-response'; +import { EVENTS, MessageHubService } from './message-hub.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {translate} from "@jsverse/transloco"; -import {DownloadableSiteTheme} from "../_models/theme/downloadable-site-theme"; -import {NgxFileDropEntry} from "ngx-file-drop"; -import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; -import {NavigationEnd, Router} from "@angular/router"; -import {ColorscapeService} from "./colorscape.service"; -import {ColorScape} from "../_models/theme/colorscape"; -import {debounceTime} from "rxjs/operators"; +import {translate} from "@ngneat/transloco"; + @Injectable({ providedIn: 'root' @@ -34,8 +28,6 @@ import {debounceTime} from "rxjs/operators"; export class ThemeService { private readonly destroyRef = inject(DestroyRef); - private readonly colorTransitionService = inject(ColorscapeService); - public defaultTheme: string = 'dark'; public defaultBookTheme: string = 'Dark'; @@ -44,9 +36,6 @@ export class ThemeService { private themesSource = new ReplaySubject(1); public themes$ = this.themesSource.asObservable(); - - private darkModeSource = new ReplaySubject(1); - public isDarkMode$ = this.darkModeSource.asObservable(); /** * Maintain a cache of themes. SignalR will inform us if we need to refresh cache @@ -58,70 +47,42 @@ export class ThemeService { constructor(rendererFactory: RendererFactory2, @Inject(DOCUMENT) private document: Document, private httpClient: HttpClient, - messageHub: MessageHubService, private domSanitizer: DomSanitizer, private confirmService: ConfirmService, private toastr: ToastrService, - private router: Router) { + messageHub: MessageHubService, private domSanitizer: DomSanitizer, private confirmService: ConfirmService, private toastr: ToastrService) { this.renderer = rendererFactory.createRenderer(null, null); messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { - if (message.event === EVENTS.NotificationProgress) { - const notificationEvent = (message.payload as NotificationProgressEvent); - if (notificationEvent.name !== EVENTS.SiteThemeProgress) return; + if (message.event !== EVENTS.NotificationProgress) return; + const notificationEvent = (message.payload as NotificationProgressEvent); + if (notificationEvent.name !== EVENTS.SiteThemeProgress) return; - if (notificationEvent.eventType === 'ended') { - if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe(); - } - return; - } - - if (message.event === EVENTS.SiteThemeUpdated) { - const evt = (message.payload as SiteThemeUpdatedEvent); - this.currentTheme$.pipe(take(1)).subscribe(currentTheme => { - if (currentTheme && currentTheme.name !== EVENTS.SiteThemeProgress) return; - console.log('Active theme has been updated, refreshing theme'); - this.setTheme(currentTheme.name); + if (notificationEvent.eventType === 'ended') { + if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe(() => { }); } - - }); } - getDownloadableThemes() { - return this.httpClient.get>(this.baseUrl + 'theme/browse'); - } - - downloadTheme(theme: DownloadableSiteTheme) { - return this.httpClient.post(this.baseUrl + 'theme/download-theme', theme); - } - - uploadTheme(themeFile: File, fileEntry: NgxFileDropEntry) { - const formData = new FormData() - formData.append('formFile', themeFile, fileEntry.relativePath); - - return this.httpClient.post(this.baseUrl + 'theme/upload-theme', formData); - } - getColorScheme() { return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim(); } - /** - * --theme-color from theme. Updates the meta tag - * @returns - */ - getThemeColor() { - return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim(); - } + /** + * --theme-color from theme. Updates the meta tag + * @returns + */ + getThemeColor() { + return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim(); + } - /** - * --msapplication-TileColor from theme. Updates the meta tag - * @returns - */ - getTileColor() { - return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim(); - } + /** + * --msapplication-TileColor from theme. Updates the meta tag + * @returns + */ + getTileColor() { + return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim(); + } getCssVariable(variable: string) { return getComputedStyle(this.document.body).getPropertyValue(variable).trim(); @@ -152,12 +113,6 @@ export class ThemeService { this.unsetThemes(); } - deleteTheme(themeId: number) { - return this.httpClient.delete(this.baseUrl + 'theme?themeId=' + themeId).pipe(map(() => { - this.getThemes().subscribe(() => {}); - })); - } - setDefault(themeId: number) { return this.httpClient.post(this.baseUrl + 'theme/update-default', {themeId: themeId}).pipe(map(() => { // Refresh the cache when a default state is changed @@ -165,6 +120,10 @@ export class ThemeService { })); } + scan() { + return this.httpClient.post(this.baseUrl + 'theme/scan', {}); + } + /** * Sets the book theme on the body tag so css variable overrides can take place * @param selector brtheme- prefixed string @@ -179,26 +138,6 @@ export class ThemeService { } - /** - * Set's the background color from a single primary color. - * @param primaryColor - * @param complementaryColor - */ - setColorScape(primaryColor: string, complementaryColor: string | null = null) { - this.colorTransitionService.setColorScape(primaryColor, complementaryColor); - } - - /** - * Trigger a request to get the colors for a given entity and apply them - * @param entity - * @param id - */ - refreshColorScape(entity: 'series' | 'volume' | 'chapter' | 'person', id: number) { - return this.httpClient.get(`${this.baseUrl}colorscape/${entity}?id=${id}`).pipe(tap((cs) => { - this.setColorScape(cs.primary || '', cs.secondary); - })); - } - /** * Sets the theme as active. Will inject a style tag into document to load a custom theme and apply the selector to the body * @param themeName @@ -209,7 +148,7 @@ export class ThemeService { this.unsetThemes(); this.renderer.addClass(this.document.querySelector('body'), theme.selector); - if (theme.provider !== ThemeProvider.System && !this.hasThemeInHead(theme.name)) { + if (theme.provider === ThemeProvider.User && !this.hasThemeInHead(theme.name)) { // We need to load the styles into the browser this.fetchThemeContent(theme.id).subscribe(async (content) => { if (content === null) { @@ -220,6 +159,7 @@ export class ThemeService { const styleElem = this.document.createElement('style'); styleElem.id = 'theme-' + theme.name; styleElem.appendChild(this.document.createTextNode(content)); + this.renderer.appendChild(this.document.head, styleElem); // Check if the theme has --theme-color and apply it to meta tag @@ -240,11 +180,9 @@ export class ThemeService { } this.currentThemeSource.next(theme); - this.darkModeSource.next(this.isDarkTheme()); }); } else { this.currentThemeSource.next(theme); - this.darkModeSource.next(this.isDarkTheme()); } } else { // Only time themes isn't already loaded is on first load @@ -272,4 +210,6 @@ export class ThemeService { private unsetBookThemes() { Array.from(this.document.body.classList).filter(cls => cls.startsWith('brtheme-')).forEach(c => this.document.body.classList.remove(c)); } + + } diff --git a/UI/Web/src/app/_services/toggle.service.ts b/UI/Web/src/app/_services/toggle.service.ts index 0ad9813e3..8b335394a 100644 --- a/UI/Web/src/app/_services/toggle.service.ts +++ b/UI/Web/src/app/_services/toggle.service.ts @@ -1,6 +1,6 @@ -import {Injectable} from '@angular/core'; -import {NavigationStart, Router} from '@angular/router'; -import {filter, ReplaySubject, take} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { NavigationStart, Router } from '@angular/router'; +import { filter, ReplaySubject, take } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -29,7 +29,7 @@ export class ToggleService { this.toggleState = !state; this.toggleStateSource.next(this.toggleState); }); - + } set(state: boolean) { diff --git a/UI/Web/src/app/_services/upload.service.ts b/UI/Web/src/app/_services/upload.service.ts index f2a811161..8c3d6295a 100644 --- a/UI/Web/src/app/_services/upload.service.ts +++ b/UI/Web/src/app/_services/upload.service.ts @@ -1,10 +1,7 @@ import { HttpClient } from '@angular/common/http'; -import {inject, Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { TextResonse } from '../_types/text-response'; -import {translate} from "@jsverse/transloco"; -import {ToastrService} from "ngx-toastr"; -import {tap} from "rxjs"; @Injectable({ providedIn: 'root' @@ -12,7 +9,6 @@ import {tap} from "rxjs"; export class UploadService { private baseUrl = environment.apiUrl; - private readonly toastr = inject(ToastrService); constructor(private httpClient: HttpClient) { } @@ -22,52 +18,29 @@ export class UploadService { } /** - * + * * @param seriesId Series to overwrite cover image for * @param url A base64 encoded url - * @param lockCover Should the cover be locked or not - * @returns + * @returns */ - updateSeriesCoverImage(seriesId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/series', {id: seriesId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); + updateSeriesCoverImage(seriesId: number, url: string) { + return this.httpClient.post(this.baseUrl + 'upload/series', {id: seriesId, url: this._cleanBase64Url(url)}); } - updateCollectionCoverImage(tagId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/collection', {id: tagId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); + updateCollectionCoverImage(tagId: number, url: string) { + return this.httpClient.post(this.baseUrl + 'upload/collection', {id: tagId, url: this._cleanBase64Url(url)}); } - updateReadingListCoverImage(readingListId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/reading-list', {id: readingListId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); + updateReadingListCoverImage(readingListId: number, url: string) { + return this.httpClient.post(this.baseUrl + 'upload/reading-list', {id: readingListId, url: this._cleanBase64Url(url)}); } - updateChapterCoverImage(chapterId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/chapter', {id: chapterId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); + updateChapterCoverImage(chapterId: number, url: string) { + return this.httpClient.post(this.baseUrl + 'upload/chapter', {id: chapterId, url: this._cleanBase64Url(url)}); } - updateVolumeCoverImage(volumeId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/volume', {id: volumeId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); - } - - updateLibraryCoverImage(libraryId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/library', {id: libraryId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); - } - - updatePersonCoverImage(personId: number, url: string, lockCover: boolean = true) { - return this.httpClient.post(this.baseUrl + 'upload/person', {id: personId, url: this._cleanBase64Url(url), lockCover}).pipe(tap(_ => { - this.toastr.info(translate('series-detail.cover-change')); - })); + updateLibraryCoverImage(libraryId: number, url: string) { + return this.httpClient.post(this.baseUrl + 'upload/library', {id: libraryId, url: this._cleanBase64Url(url)}); } resetChapterCoverLock(chapterId: number, ) { diff --git a/UI/Web/src/app/_services/version.service.ts b/UI/Web/src/app/_services/version.service.ts deleted file mode 100644 index ed9d8bac6..000000000 --- a/UI/Web/src/app/_services/version.service.ts +++ /dev/null @@ -1,250 +0,0 @@ -import {inject, Injectable, OnDestroy} from '@angular/core'; -import {interval, Subscription, switchMap} from 'rxjs'; -import {ServerService} from "./server.service"; -import {AccountService} from "./account.service"; -import {filter, take} from "rxjs/operators"; -import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; -import {NewUpdateModalComponent} from "../announcements/_components/new-update-modal/new-update-modal.component"; -import {OutOfDateModalComponent} from "../announcements/_components/out-of-date-modal/out-of-date-modal.component"; -import {Router} from "@angular/router"; - -@Injectable({ - providedIn: 'root' -}) -export class VersionService implements OnDestroy{ - - private readonly serverService = inject(ServerService); - private readonly accountService = inject(AccountService); - private readonly modalService = inject(NgbModal); - private readonly router = inject(Router); - - public static readonly SERVER_VERSION_KEY = 'kavita--version'; - public static readonly CLIENT_REFRESH_KEY = 'kavita--client-refresh-last-shown'; - public static readonly NEW_UPDATE_KEY = 'kavita--new-update-last-shown'; - public static readonly OUT_OF_BAND_KEY = 'kavita--out-of-band-last-shown'; - - // Notification intervals - private readonly CLIENT_REFRESH_INTERVAL = 0; // Show immediately (once) - private readonly NEW_UPDATE_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week in milliseconds - private readonly OUT_OF_BAND_INTERVAL = 30 * 24 * 60 * 60 * 1000; // 1 month in milliseconds - - // Check intervals - private readonly VERSION_CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes - private readonly OUT_OF_DATE_CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 2 hours - private readonly OUT_Of_BAND_AMOUNT = 3; // How many releases before we show "You're X releases out of date" - - // Routes where version update modals should not be shown - private readonly EXCLUDED_ROUTES = [ - '/manga/', - '/book/', - '/pdf/', - '/reader/' - ]; - - - private versionCheckSubscription?: Subscription; - private outOfDateCheckSubscription?: Subscription; - private modalOpen = false; - - constructor() { - this.startInitialVersionCheck(); - this.startVersionCheck(); - this.startOutOfDateCheck(); - } - - ngOnDestroy() { - this.versionCheckSubscription?.unsubscribe(); - this.outOfDateCheckSubscription?.unsubscribe(); - } - - /** - * Initial version check to ensure localStorage is populated on first load - */ - private startInitialVersionCheck(): void { - this.accountService.currentUser$ - .pipe( - filter(user => !!user), - take(1), - switchMap(user => this.serverService.getVersion(user!.apiKey)) - ) - .subscribe(serverVersion => { - const cachedVersion = localStorage.getItem(VersionService.SERVER_VERSION_KEY); - - // Always update localStorage on first load - localStorage.setItem(VersionService.SERVER_VERSION_KEY, serverVersion); - - console.log('Initial version check - Server version:', serverVersion, 'Cached version:', cachedVersion); - }); - } - - /** - * Periodic check for server version to detect client refreshes and new updates - */ - private startVersionCheck(): void { - console.log('Starting version checker'); - this.versionCheckSubscription = interval(this.VERSION_CHECK_INTERVAL) - .pipe( - switchMap(() => this.accountService.currentUser$), - filter(user => !!user && !this.modalOpen), - switchMap(user => this.serverService.getVersion(user!.apiKey)), - filter(update => !!update), - ).subscribe(version => this.handleVersionUpdate(version)); - } - - /** - * Checks if the server is out of date compared to the latest release - */ - private startOutOfDateCheck() { - console.log('Starting out-of-date checker'); - this.outOfDateCheckSubscription = interval(this.OUT_OF_DATE_CHECK_INTERVAL) - .pipe( - switchMap(() => this.accountService.currentUser$), - filter(u => u !== undefined && this.accountService.hasAdminRole(u) && !this.modalOpen), - switchMap(_ => this.serverService.checkHowOutOfDate(true)), - filter(versionsOutOfDate => !isNaN(versionsOutOfDate) && versionsOutOfDate > this.OUT_Of_BAND_AMOUNT), - ) - .subscribe(versionsOutOfDate => this.handleOutOfDateNotification(versionsOutOfDate)); - } - - /** - * Checks if the current route is in the excluded routes list - */ - private isExcludedRoute(): boolean { - const currentUrl = this.router.url; - return this.EXCLUDED_ROUTES.some(route => currentUrl.includes(route)); - } - - /** - * Handles the version check response to determine if client refresh or new update notification is needed - */ - private handleVersionUpdate(serverVersion: string) { - if (this.modalOpen) return; - - // Validate if we are on a reader route and if so, suppress - if (this.isExcludedRoute()) { - console.log('Version update blocked due to user reading'); - return; - } - - const cachedVersion = localStorage.getItem(VersionService.SERVER_VERSION_KEY); - console.log('Server version:', serverVersion, 'Cached version:', cachedVersion); - - const isNewServerVersion = cachedVersion !== null && cachedVersion !== serverVersion; - - // Case 1: Client Refresh needed (server has updated since last client load) - if (isNewServerVersion) { - this.showClientRefreshNotification(serverVersion); - } - // Case 2: Check for new updates (for server admin) - else { - this.checkForNewUpdates(); - } - - // Always update the cached version - localStorage.setItem(VersionService.SERVER_VERSION_KEY, serverVersion); - } - - /** - * Shows a notification that client refresh is needed due to server update - */ - private showClientRefreshNotification(newVersion: string): void { - this.pauseChecks(); - - // Client refresh notifications should always show (once) - this.modalOpen = true; - - this.serverService.getChangelog(1).subscribe(changelog => { - const ref = this.modalService.open(NewUpdateModalComponent, { - size: 'lg', - keyboard: false, - backdrop: 'static' // Prevent closing by clicking outside - }); - - ref.componentInstance.version = newVersion; - ref.componentInstance.update = changelog[0]; - ref.componentInstance.requiresRefresh = true; - - // Update the last shown timestamp - localStorage.setItem(VersionService.CLIENT_REFRESH_KEY, Date.now().toString()); - - ref.closed.subscribe(_ => this.onModalClosed()); - ref.dismissed.subscribe(_ => this.onModalClosed()); - }); - } - - /** - * Checks for new server updates and shows notification if appropriate - */ - private checkForNewUpdates(): void { - this.accountService.currentUser$ - .pipe( - take(1), - filter(user => user !== undefined && this.accountService.hasAdminRole(user)), - switchMap(_ => this.serverService.checkHowOutOfDate()), - filter(versionsOutOfDate => !isNaN(versionsOutOfDate) && versionsOutOfDate > 0 && versionsOutOfDate <= this.OUT_Of_BAND_AMOUNT) - ) - .subscribe(versionsOutOfDate => { - const lastShown = Number(localStorage.getItem(VersionService.NEW_UPDATE_KEY) || '0'); - const currentTime = Date.now(); - - // Show notification if it hasn't been shown in the last week - if (currentTime - lastShown >= this.NEW_UPDATE_INTERVAL) { - this.pauseChecks(); - this.modalOpen = true; - - this.serverService.getChangelog(1).subscribe(changelog => { - const ref = this.modalService.open(NewUpdateModalComponent, { size: 'lg' }); - ref.componentInstance.versionsOutOfDate = versionsOutOfDate; - ref.componentInstance.update = changelog[0]; - ref.componentInstance.requiresRefresh = false; - - // Update the last shown timestamp - localStorage.setItem(VersionService.NEW_UPDATE_KEY, currentTime.toString()); - - ref.closed.subscribe(_ => this.onModalClosed()); - ref.dismissed.subscribe(_ => this.onModalClosed()); - }); - } - }); - } - - /** - * Handles the notification for servers that are significantly out of date - */ - private handleOutOfDateNotification(versionsOutOfDate: number): void { - const lastShown = Number(localStorage.getItem(VersionService.OUT_OF_BAND_KEY) || '0'); - const currentTime = Date.now(); - - // Show notification if it hasn't been shown in the last month - if (currentTime - lastShown >= this.OUT_OF_BAND_INTERVAL) { - this.pauseChecks(); - this.modalOpen = true; - - const ref = this.modalService.open(OutOfDateModalComponent, { size: 'xl', fullscreen: 'md' }); - ref.componentInstance.versionsOutOfDate = versionsOutOfDate; - - // Update the last shown timestamp - localStorage.setItem(VersionService.OUT_OF_BAND_KEY, currentTime.toString()); - - ref.closed.subscribe(_ => this.onModalClosed()); - ref.dismissed.subscribe(_ => this.onModalClosed()); - } - } - - /** - * Pauses all version checks while modals are open - */ - private pauseChecks(): void { - this.versionCheckSubscription?.unsubscribe(); - this.outOfDateCheckSubscription?.unsubscribe(); - } - - /** - * Resumes all checks when modals are closed - */ - private onModalClosed(): void { - this.modalOpen = false; - this.startVersionCheck(); - this.startOutOfDateCheck(); - } -} diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts deleted file mode 100644 index 8c9f9e17e..000000000 --- a/UI/Web/src/app/_services/volume.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@angular/core'; -import {environment} from "../../environments/environment"; -import { HttpClient } from "@angular/common/http"; -import {Volume} from "../_models/volume"; -import {TextResonse} from "../_types/text-response"; - -@Injectable({ - providedIn: 'root' -}) -export class VolumeService { - - baseUrl = environment.apiUrl; - - constructor(private httpClient: HttpClient) { } - - getVolumeMetadata(volumeId: number) { - return this.httpClient.get(this.baseUrl + 'volume?volumeId=' + volumeId); - } - - deleteVolume(volumeId: number) { - return this.httpClient.delete(this.baseUrl + 'volume?volumeId=' + volumeId); - } - - deleteMultipleVolumes(volumeIds: number[]) { - return this.httpClient.post(this.baseUrl + "volume/multiple", volumeIds) - } - - updateVolume(volume: any) { - return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse); - } - -} diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html deleted file mode 100644 index 7573c554a..000000000 --- a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts deleted file mode 100644 index 9777abc1d..000000000 --- a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - EventEmitter, - inject, - Input, - OnInit, - Output -} from '@angular/core'; -import {translate, TranslocoDirective} from "@jsverse/transloco"; -import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; -import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; -import {ActionableEntity, ActionItem} from "../../_services/action-factory.service"; -import {AccountService} from "../../_services/account.service"; -import {tap} from "rxjs"; -import {User} from "../../_models/user"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; - -@Component({ - selector: 'app-actionable-modal', - imports: [ - TranslocoDirective - ], - templateUrl: './actionable-modal.component.html', - styleUrl: './actionable-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ActionableModalComponent implements OnInit { - - protected readonly utilityService = inject(UtilityService); - protected readonly modal = inject(NgbActiveModal); - protected readonly accountService = inject(AccountService); - protected readonly cdRef = inject(ChangeDetectorRef); - protected readonly destroyRef = inject(DestroyRef); - protected readonly Breakpoint = Breakpoint; - - @Input() entity: ActionableEntity = null; - @Input() actions: ActionItem[] = []; - @Input() willRenderAction!: (action: ActionItem, user: User) => boolean; - @Input() shouldRenderSubMenu!: (action: ActionItem, dynamicList: null | Array) => boolean; - @Output() actionPerformed = new EventEmitter>(); - - currentLevel: string[] = []; - currentItems: ActionItem[] = []; - user!: User | undefined; - - ngOnInit() { - this.currentItems = this.translateOptions(this.actions); - - this.accountService.currentUser$.pipe(tap(user => { - this.user = user; - this.cdRef.markForCheck(); - }), takeUntilDestroyed(this.destroyRef)).subscribe(); - } - - handleItemClick(item: ActionItem) { - if (item.children && item.children.length > 0) { - this.currentLevel.push(item.title); - - if (item.children.length === 1 && item.children[0].dynamicList) { - item.children[0].dynamicList.subscribe(dynamicItems => { - this.currentItems = dynamicItems.map(di => ({ - ...item, - children: [], // Required as dynamic list is only one deep - title: di.title, - _extra: di, - action: item.children[0].action // override action to be correct from child - })); - }); - } else { - this.currentItems = this.translateOptions(item.children); - } - } - else { - this.actionPerformed.emit(item); - this.modal.close(item); - } - this.cdRef.markForCheck(); - } - - handleBack() { - if (this.currentLevel.length > 0) { - this.currentLevel.pop(); - - let items = this.actions; - for (let level of this.currentLevel) { - items = items.find(item => item.title === level)?.children || []; - } - - this.currentItems = this.translateOptions(items); - this.cdRef.markForCheck(); - } - } - - translateOptions(opts: Array>) { - return opts.map(a => { - return {...a, title: translate('actionable.' + a.title)}; - }) - } - -} diff --git a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html deleted file mode 100644 index 2be9b618d..000000000 --- a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.ts b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.ts deleted file mode 100644 index ebd4b2900..000000000 --- a/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - inject, - Input, - OnChanges, - OnInit, - SimpleChanges -} from '@angular/core'; -import {AgeRating} from "../../_models/metadata/age-rating"; -import {ImageComponent} from "../../shared/image/image.component"; -import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {AgeRatingPipe} from "../../_pipes/age-rating.pipe"; -import {AsyncPipe} from "@angular/common"; -import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service"; -import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; - -const basePath = './assets/images/ratings/'; - -@Component({ - selector: 'app-age-rating-image', - imports: [ - ImageComponent, - NgbTooltip, - AgeRatingPipe, - ], - templateUrl: './age-rating-image.component.html', - styleUrl: './age-rating-image.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class AgeRatingImageComponent implements OnInit, OnChanges { - private readonly cdRef = inject(ChangeDetectorRef); - private readonly filterUtilityService = inject(FilterUtilitiesService); - - protected readonly AgeRating = AgeRating; - - @Input({required: true}) rating: AgeRating = AgeRating.Unknown; - - imageUrl: string = 'unknown-rating.png'; - - ngOnInit() { - this.setImage(); - } - - ngOnChanges() { - this.setImage(); - } - - setImage() { - switch (this.rating) { - case AgeRating.Unknown: - this.imageUrl = basePath + 'unknown-rating.png'; - break; - case AgeRating.RatingPending: - this.imageUrl = basePath + 'rating-pending-rating.png'; - break; - case AgeRating.EarlyChildhood: - this.imageUrl = basePath + 'early-childhood-rating.png'; - break; - case AgeRating.Everyone: - this.imageUrl = basePath + 'everyone-rating.png'; - break; - case AgeRating.G: - this.imageUrl = basePath + 'g-rating.png'; - break; - case AgeRating.Everyone10Plus: - this.imageUrl = basePath + 'everyone-10+-rating.png'; - break; - case AgeRating.PG: - this.imageUrl = basePath + 'pg-rating.png'; - break; - case AgeRating.KidsToAdults: - this.imageUrl = basePath + 'kids-to-adults-rating.png'; - break; - case AgeRating.Teen: - this.imageUrl = basePath + 'teen-rating.png'; - break; - case AgeRating.Mature15Plus: - this.imageUrl = basePath + 'ma15+-rating.png'; - break; - case AgeRating.Mature17Plus: - this.imageUrl = basePath + 'mature-17+-rating.png'; - break; - case AgeRating.Mature: - this.imageUrl = basePath + 'm-rating.png'; - break; - case AgeRating.R18Plus: - this.imageUrl = basePath + 'r18+-rating.png'; - break; - case AgeRating.AdultsOnly: - this.imageUrl = basePath + 'adults-only-18+-rating.png'; - break; - case AgeRating.X18Plus: - this.imageUrl = basePath + 'x18+-rating.png'; - break; - } - this.cdRef.markForCheck(); - } - - openRating() { - this.filterUtilityService.applyFilter(['all-series'], FilterField.AgeRating, FilterComparison.Equal, `${this.rating}`).subscribe(); - } - - -} diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html index 3b5c44117..f2452ea19 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html @@ -1,57 +1,40 @@ - - @if (actions.length > 0) { - @if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) { - - } @else { -
- -
- -
-
- - @for(action of list; track action.title) { - - @if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) { - @if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) { - @for(dynamicItem of dList; track dynamicItem.title) { - - } - } @else if (willRenderAction(action, this.currentUser!)) { - - } - } @else { - @if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async) && hasRenderableChildren(action, this.currentUser!)) { - - - } - } - } + + + +
+ + + +
+ +
+ +
+
+
- } - } + + + + diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss index 19a986986..5768c28f8 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.scss @@ -2,22 +2,6 @@ content: none !important; } -.submenu-wrapper { - position: relative; - - &::after { - content: ''; - position: absolute; - top: 0; - right: -10px; - width: 10px; - height: 100%; - background: transparent; - cursor: pointer; - pointer-events: auto; - } -} - .submenu-toggle { display: block; width: 100%; @@ -42,7 +26,3 @@ float: right; padding: var(--bs-dropdown-item-padding-y) 0; } - -.btn { - padding: 5px; -} diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts index 3e3522d5f..e9e9952dc 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.ts @@ -1,113 +1,67 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - EventEmitter, - inject, - Input, - OnChanges, - OnDestroy, - OnInit, - Output -} from '@angular/core'; -import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import {AccountService} from 'src/app/_services/account.service'; -import {ActionableEntity, ActionItem} from 'src/app/_services/action-factory.service'; -import {AsyncPipe, NgTemplateOutlet} from "@angular/common"; -import {TranslocoDirective} from "@jsverse/transloco"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '@ng-bootstrap/ng-bootstrap'; +import { take } from 'rxjs'; +import { AccountService } from 'src/app/_services/account.service'; +import { Action, ActionItem } from 'src/app/_services/action-factory.service'; +import {CommonModule} from "@angular/common"; +import {TranslocoDirective} from "@ngneat/transloco"; import {DynamicListPipe} from "./_pipes/dynamic-list.pipe"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; -import {ActionableModalComponent} from "../actionable-modal/actionable-modal.component"; -import {User} from "../../_models/user"; - @Component({ selector: 'app-card-actionables', - imports: [ - NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, - DynamicListPipe, TranslocoDirective, AsyncPipe, NgTemplateOutlet - ], + standalone: true, + imports: [CommonModule, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, DynamicListPipe, TranslocoDirective], templateUrl: './card-actionables.component.html', styleUrls: ['./card-actionables.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CardActionablesComponent implements OnInit, OnChanges, OnDestroy { - - private readonly cdRef = inject(ChangeDetectorRef); - private readonly accountService = inject(AccountService); - private readonly destroyRef = inject(DestroyRef); - protected readonly utilityService = inject(UtilityService); - protected readonly modalService = inject(NgbModal); - - protected readonly Breakpoint = Breakpoint; +export class CardActionablesComponent implements OnInit { @Input() iconClass = 'fa-ellipsis-v'; @Input() btnClass = ''; - @Input() inputActions: ActionItem[] = []; + @Input() actions: ActionItem[] = []; @Input() labelBy = 'card'; - /** - * Text to display as if actionable was a button - */ - @Input() label = ''; @Input() disabled: boolean = false; - - @Input() entity: ActionableEntity = null; - /** - * This will only emit when the action is clicked and the entity is null. Otherwise, the entity callback handler will be invoked. - */ @Output() actionHandler = new EventEmitter>(); - actions: ActionItem[] = []; - currentUser: User | undefined = undefined; + isAdmin: boolean = false; + canDownload: boolean = false; submenu: {[key: string]: NgbDropdown} = {}; - private closeTimeout: any = null; + constructor(private readonly cdRef: ChangeDetectorRef, private accountService: AccountService) { } ngOnInit(): void { - this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((user) => { + this.accountService.currentUser$.pipe(take(1)).subscribe((user) => { if (!user) return; - this.currentUser = user; - this.actions = this.inputActions.filter(a => this.willRenderAction(a, user!)); + this.isAdmin = this.accountService.hasAdminRole(user); + this.canDownload = this.accountService.hasDownloadRole(user); + + // We want to avoid an empty menu when user doesn't have access to anything + if (!this.isAdmin && this.actions.filter(a => !a.requiresAdmin).length === 0) { + this.actions = []; + } this.cdRef.markForCheck(); }); } - ngOnChanges() { - this.actions = this.inputActions.filter(a => this.willRenderAction(a, this.currentUser!)); - this.cdRef.markForCheck(); - } - - ngOnDestroy() { - this.cancelCloseSubmenus(); - } - preventEvent(event: any) { event.stopPropagation(); event.preventDefault(); } - performAction(event: any, action: ActionItem) { + performAction(event: any, action: ActionItem) { this.preventEvent(event); if (typeof action.callback === 'function') { - if (this.entity === null) { - this.actionHandler.emit(action); - } else { - action.callback(action, this.entity); - } + this.actionHandler.emit(action); } } - /** - * The user has required roles (or no roles defined) and action shouldRender returns true - * @param action - * @param user - */ - willRenderAction(action: ActionItem, user: User) { - return (!action.requiredRoles?.length || this.accountService.hasAnyRole(user, action.requiredRoles)) && action.shouldRender(action, this.entity, user); + willRenderAction(action: ActionItem) { + return (action.requiresAdmin && this.isAdmin) + || (action.action === Action.Download && (this.canDownload || this.isAdmin)) + || (!action.requiresAdmin && action.action !== Action.Download); } shouldRenderSubMenu(action: ActionItem, dynamicList: null | Array) { @@ -128,55 +82,14 @@ export class CardActionablesComponent implements OnInit, OnChanges, OnDestroy { } closeAllSubmenus() { - // Clear any existing timeout to avoid race conditions - if (this.closeTimeout) { - clearTimeout(this.closeTimeout); - } - - // Set a new timeout to close submenus after a short delay - this.closeTimeout = setTimeout(() => { - Object.keys(this.submenu).forEach(key => { - this.submenu[key].close(); + Object.keys(this.submenu).forEach(key => { + this.submenu[key].close(); delete this.submenu[key]; - }); - }, 100); // Small delay to prevent premature closing (dropdown tunneling) + }); } - cancelCloseSubmenus() { - if (this.closeTimeout) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; - } - } - - hasRenderableChildren(action: ActionItem, user: User): boolean { - if (!action.children || action.children.length === 0) return false; - - for (const child of action.children) { - const dynamicList = child.dynamicList; - if (dynamicList !== undefined) return true; // Dynamic list gets rendered if loaded - - if (this.willRenderAction(child, user)) return true; - if (child.children?.length && this.hasRenderableChildren(child, user)) return true; - } - return false; - } - - performDynamicClick(event: any, action: ActionItem, dynamicItem: any) { + performDynamicClick(event: any, action: ActionItem, dynamicItem: any) { action._extra = dynamicItem; this.performAction(event, action); } - - openMobileActionableMenu(event: any) { - this.preventEvent(event); - - const ref = this.modalService.open(ActionableModalComponent, {fullscreen: true, centered: true}); - ref.componentInstance.entity = this.entity; - ref.componentInstance.actions = this.actions; - ref.componentInstance.willRenderAction = this.willRenderAction.bind(this); - ref.componentInstance.shouldRenderSubMenu = this.shouldRenderSubMenu.bind(this); - ref.componentInstance.actionPerformed.subscribe((action: ActionItem) => { - this.performAction(event, action); - }); - } } diff --git a/UI/Web/src/app/_single-module/cover-image/cover-image.component.html b/UI/Web/src/app/_single-module/cover-image/cover-image.component.html deleted file mode 100644 index 18a137cfa..000000000 --- a/UI/Web/src/app/_single-module/cover-image/cover-image.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - @if(mobileSeriesImgBackground === 'true') { - - } @else { - - } -
-
-
- - -
- -
-
-
-
- - @if (entity.pagesRead < entity.pages && entity.pagesRead > 0) { -
- -
- @if (continueTitle !== '') { -
-
- {{t('continue-from', {title: continueTitle})}} -
-
- } - } -
diff --git a/UI/Web/src/app/_single-module/cover-image/cover-image.component.scss b/UI/Web/src/app/_single-module/cover-image/cover-image.component.scss deleted file mode 100644 index 0c9c9d032..000000000 --- a/UI/Web/src/app/_single-module/cover-image/cover-image.component.scss +++ /dev/null @@ -1,146 +0,0 @@ -.overlay-information { - position: relative; - top: -364px; - height: 364px; - transition: all 0.2s; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - - &:hover { - cursor: pointer; - background-color: var(--card-overlay-hover-bg-color) !important; - - .overlay-information--centered { - visibility: visible; - } - } - - .overlay-information--centered { - position: absolute; - border-radius: 15px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 50px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 115; - visibility: hidden; - - &:hover { - background-color: var(--primary-color) !important; - cursor: pointer; - } - - div { - width: 60px; - height: 60px; - i { - font-size: 1.6rem; - line-height: 60px; - width: 100%; - } - } - } -} - -.overlay-information { - position: absolute; - top: 0; - left: 12px; - width: calc(100% - 24px); - height: 100%; - transition: all 0.2s; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - - &:hover { - background-color: var(--card-overlay-hover-bg-color); - cursor: pointer; - } - - .overlay-information--centered { - position: absolute; - border-radius: 15px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 50px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 115; - - &:hover { - background-color: var(--primary-color) !important; - cursor: pointer; - } - } -} - -.series { - .overlay-information--centered { - div { - height: 32px; - width: 32px; - i { - font-size: 1.4rem; - line-height: 32px; - } - } - } -} - -::ng-deep .image-container app-image img { - border-radius: 4px 4px 0 0; -} - -.progress { - border-radius: 0; -} - -.progress-banner.series { - position: relative; -} - -::ng-deep .progress-banner.series span { - position: absolute; - left: 50%; - transform: translate(-50%, -50%); - color: white; - top: 50%; -} - -.under-image { - position: relative; - - .continue-from { - background-color: var(--breadcrumb-bg-color); - color: white; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - text-align: center; - position: absolute; - width: 100%; - font-size: 0.8rem; - -webkit-line-clamp: 1; - font-size: 0.8rem; - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - padding: 0 10px 0 0; - } -} - -@media screen and (max-width: 991px) { - .overlay-information { - visibility: hidden; - .overlay-information--centered { - visibility: hidden !important; - } - } - .progress-banner { - display: none; - } - - .under-image { - display: none; - } -} diff --git a/UI/Web/src/app/_single-module/cover-image/cover-image.component.ts b/UI/Web/src/app/_single-module/cover-image/cover-image.component.ts deleted file mode 100644 index fa8996428..000000000 --- a/UI/Web/src/app/_single-module/cover-image/cover-image.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; -import {DecimalPipe, NgClass} from "@angular/common"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {ImageComponent} from "../../shared/image/image.component"; -import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; -import {IHasProgress} from "../../_models/common/i-has-progress"; - -/** - * Used for the Series/Volume/Chapter Detail pages - */ -@Component({ - selector: 'app-cover-image', - imports: [ - TranslocoDirective, - ImageComponent, - NgbProgressbar, - DecimalPipe, - NgbTooltip - ], - templateUrl: './cover-image.component.html', - styleUrl: './cover-image.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class CoverImageComponent { - - @Input({required: true}) coverImage!: string; - @Input({required: true}) entity!: IHasProgress; - @Input() continueTitle: string = ''; - @Output() read = new EventEmitter(); - - mobileSeriesImgBackground = getComputedStyle(document.documentElement) - .getPropertyValue('--mobile-series-img-background').trim(); - -} diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html deleted file mode 100644 index 1087f3d3b..000000000 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ /dev/null @@ -1,191 +0,0 @@ - -
- - @if (readingTime) { -
-

{{t('read-time-title')}}

-
- {{readingTime | readTime}} -
-
- } - - @if (releaseYear) { -
-

{{t('release-title')}}

-
- {{releaseYear}} -
-
- } - - @if (language) { -
-

{{t('language-title')}}

-
- {{language | languageName | async}} -
-
- } - - @if (ageRating) { -
-

{{t('age-rating-title')}}

-
- -
-
- } - - @if (format) { -
-

{{t('format-title')}}

-
- {{format | mangaFormat }} -
-
- } - - - @if (!suppressEmptyGenres || genres.length > 0) { - - -
-

{{t('genres-title')}}

-
- - - {{item.title}} - - -
-
- } - - @if (!suppressEmptyTags || tags.length > 0) { -
-

{{t('tags-title')}}

-
- - - {{item.title}} - - -
-
- } - -
- - - - - - - -
- - @if (genres.length > 0 || tags.length > 0 || webLinks.length > 0) { - - } - - -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- - -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
- -
- - - - - -
-
-
diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts deleted file mode 100644 index 096826964..000000000 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; -import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component"; -import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {IHasCast} from "../../_models/common/i-has-cast"; -import {PersonRole} from "../../_models/metadata/person"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; -import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; -import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service"; -import {Genre} from "../../_models/metadata/genre"; -import {Tag} from "../../_models/tag"; -import {ImageComponent} from "../../shared/image/image.component"; -import {ImageService} from "../../_services/image.service"; -import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component"; -import {IHasReadingTime} from "../../_models/common/i-has-reading-time"; -import {ReadTimePipe} from "../../_pipes/read-time.pipe"; -import {MangaFormat} from "../../_models/manga-format"; -import {SeriesFormatComponent} from "../../shared/series-format/series-format.component"; -import {MangaFormatPipe} from "../../_pipes/manga-format.pipe"; -import {LanguageNamePipe} from "../../_pipes/language-name.pipe"; -import {AsyncPipe} from "@angular/common"; -import {SafeUrlPipe} from "../../_pipes/safe-url.pipe"; -import {AgeRating} from "../../_models/metadata/age-rating"; -import {AgeRatingImageComponent} from "../age-rating-image/age-rating-image.component"; - -@Component({ - selector: 'app-details-tab', - imports: [ - CarouselReelComponent, - PersonBadgeComponent, - TranslocoDirective, - ImageComponent, - BadgeExpanderComponent, - ReadTimePipe, - SeriesFormatComponent, - MangaFormatPipe, - LanguageNamePipe, - AsyncPipe, - SafeUrlPipe, - AgeRatingImageComponent - ], - templateUrl: './details-tab.component.html', - styleUrl: './details-tab.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DetailsTabComponent { - - protected readonly imageService = inject(ImageService); - private readonly filterUtilityService = inject(FilterUtilitiesService); - - protected readonly PersonRole = PersonRole; - protected readonly FilterField = FilterField; - protected readonly MangaFormat = MangaFormat; - - @Input({required: true}) metadata!: IHasCast; - @Input() readingTime: IHasReadingTime | undefined; - @Input() ageRating: AgeRating | undefined; - @Input() language: string | undefined; - @Input() format: MangaFormat | undefined; - @Input() releaseYear: number | undefined; - @Input() genres: Array = []; - @Input() tags: Array = []; - @Input() webLinks: Array = []; - @Input() suppressEmptyGenres: boolean = false; - @Input() suppressEmptyTags: boolean = false; - - - openGeneric(queryParamName: FilterField, filter: string | number) { - if (queryParamName === FilterField.None) return; - this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe(); - } -} diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html deleted file mode 100644 index 979794d20..000000000 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html +++ /dev/null @@ -1,666 +0,0 @@ - - - - - - - - {{t('field-locked-alt')}} - - - - - diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.scss b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.scss deleted file mode 100644 index fcef7bfcb..000000000 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -.lock-active { - > .input-group-text { - background-color: var(--primary-color); - color: white; - } -} diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts deleted file mode 100644 index bda048341..000000000 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ /dev/null @@ -1,545 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; -import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; -import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; -import {AsyncPipe, NgClass, NgTemplateOutlet, TitleCasePipe} from "@angular/common"; -import {NgbActiveModal, NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {AccountService} from "../../_services/account.service"; -import {Chapter} from "../../_models/chapter"; -import {LibraryType} from "../../_models/library/library"; -import {TypeaheadSettings} from "../../typeahead/_models/typeahead-settings"; -import {Tag} from "../../_models/tag"; -import {Language} from "../../_models/metadata/language"; -import {Person, PersonRole} from "../../_models/metadata/person"; -import {Genre} from "../../_models/metadata/genre"; -import {AgeRatingDto} from "../../_models/metadata/age-rating-dto"; -import {ImageService} from "../../_services/image.service"; -import {UploadService} from "../../_services/upload.service"; -import {MetadataService} from "../../_services/metadata.service"; -import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service"; -import {ActionService} from "../../_services/action.service"; -import {DownloadService} from "../../shared/_services/download.service"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component"; -import {forkJoin, Observable, of, tap} from "rxjs"; -import {map, switchMap} from "rxjs/operators"; -import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component"; -import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; -import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; -import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; -import {MangaFormat} from "../../_models/manga-format"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {BytesPipe} from "../../_pipes/bytes.pipe"; -import {ImageComponent} from "../../shared/image/image.component"; -import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {ReadTimePipe} from "../../_pipes/read-time.pipe"; -import {ChapterService} from "../../_services/chapter.service"; -import {AgeRating} from "../../_models/metadata/age-rating"; -import {User} from "../../_models/user"; - -enum TabID { - General = 'general-tab', - CoverImage = 'cover-image-tab', - Info = 'info-tab', - People = 'people-tab', - Tasks = 'tasks-tab', - Progress = 'progress-tab', - Tags = 'tags-tab' -} - -export interface EditChapterModalCloseResult { - success: boolean; - chapter: Chapter; - coverImageUpdate: boolean; - needsReload: boolean; - isDeleted: boolean; -} - -const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; - -@Component({ - selector: 'app-edit-chapter-modal', - imports: [ - FormsModule, - NgbNav, - NgbNavContent, - NgbNavLink, - TranslocoDirective, - AsyncPipe, - NgbNavOutlet, - ReactiveFormsModule, - NgbNavItem, - SettingItemComponent, - NgTemplateOutlet, - NgClass, - TypeaheadComponent, - EntityTitleComponent, - TitleCasePipe, - SettingButtonComponent, - CoverImageChooserComponent, - EditChapterProgressComponent, - CompactNumberPipe, - DefaultDatePipe, - UtcToLocalTimePipe, - BytesPipe, - ImageComponent, - SafeHtmlPipe, - ReadTimePipe, - ], - templateUrl: './edit-chapter-modal.component.html', - styleUrl: './edit-chapter-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class EditChapterModalComponent implements OnInit { - - protected readonly modal = inject(NgbActiveModal); - public readonly utilityService = inject(UtilityService); - public readonly imageService = inject(ImageService); - private readonly uploadService = inject(UploadService); - private readonly metadataService = inject(MetadataService); - private readonly cdRef = inject(ChangeDetectorRef); - protected readonly accountService = inject(AccountService); - private readonly destroyRef = inject(DestroyRef); - private readonly actionFactoryService = inject(ActionFactoryService); - private readonly actionService = inject(ActionService); - private readonly downloadService = inject(DownloadService); - private readonly chapterService = inject(ChapterService); - - protected readonly Breakpoint = Breakpoint; - protected readonly TabID = TabID; - protected readonly Action = Action; - protected readonly PersonRole = PersonRole; - protected readonly MangaFormat = MangaFormat; - - @Input({required: true}) chapter!: Chapter; - @Input({required: true}) libraryType!: LibraryType; - @Input({required: true}) libraryId!: number; - @Input({required: true}) seriesId!: number; - - activeId = TabID.General; - editForm: FormGroup = new FormGroup({}); - selectedCover: string = ''; - coverImageReset = false; - - tagsSettings: TypeaheadSettings = new TypeaheadSettings(); - languageSettings: TypeaheadSettings = new TypeaheadSettings(); - peopleSettings: {[PersonRole: string]: TypeaheadSettings} = {}; - genreSettings: TypeaheadSettings = new TypeaheadSettings(); - - tags: Tag[] = []; - genres: Genre[] = []; - ageRatings: Array = []; - validLanguages: Array = []; - - tasks = this.actionFactoryService.getActionablesForSettingsPage(this.actionFactoryService.getChapterActions(this.runTask.bind(this)), blackList); - /** - * A copy of the chapter from init. This is used to compare values for name fields to see if lock was modified - */ - initChapter!: Chapter; - imageUrls: Array = []; - size: number = 0; - user!: User; - - get WebLinks() { - if (this.chapter.webLinks === '') return []; - return this.chapter.webLinks.split(','); - } - - - - ngOnInit() { - this.initChapter = Object.assign({}, this.chapter); - this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id)); - - this.size = this.utilityService.asChapter(this.chapter).files.reduce((sum, v) => sum + v.bytes, 0); - this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), tap(u => { - if (!u) return; - this.user = u; - - if (!this.accountService.hasAdminRole(this.user)) { - this.activeId = TabID.Info; - } - this.cdRef.markForCheck(); - - })).subscribe(); - - this.editForm.addControl('titleName', new FormControl(this.chapter.titleName, [])); - this.editForm.addControl('sortOrder', new FormControl(Math.max(0, this.chapter.sortOrder), [Validators.required, Validators.min(0)])); - this.editForm.addControl('summary', new FormControl(this.chapter.summary || '', [])); - this.editForm.addControl('language', new FormControl(this.chapter.language, [])); - this.editForm.addControl('isbn', new FormControl(this.chapter.isbn, [])); - this.editForm.addControl('ageRating', new FormControl(this.chapter.ageRating, [])); - - if (this.chapter.releaseDate !== '0001-01-01T00:00:00') { - this.editForm.addControl('releaseDate', new FormControl(this.chapter.releaseDate.substring(0, 10), [])); - } else { - this.editForm.addControl('releaseDate', new FormControl('', [])); - } - - - this.editForm.addControl('genres', new FormControl(this.chapter.genres, [])); - this.editForm.addControl('tags', new FormControl(this.chapter.tags, [])); - - - this.editForm.addControl('coverImageIndex', new FormControl(0, [])); - this.editForm.addControl('coverImageLocked', new FormControl(this.chapter.coverImageLocked, [])); - - this.metadataService.getAllValidLanguages().pipe( - tap(validLanguages => { - this.validLanguages = validLanguages; - this.cdRef.markForCheck(); - }), - switchMap(_ => this.setupLanguageTypeahead()) - ).subscribe(); - - this.metadataService.getAllAgeRatings().subscribe(ratings => { - this.ageRatings = ratings; - this.cdRef.markForCheck(); - }); - - this.editForm.get('titleName')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.titleNameLocked = true; - this.cdRef.markForCheck(); - }); - - this.editForm.get('sortOrder')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.sortOrderLocked = true; - this.cdRef.markForCheck(); - }); - - this.editForm.get('isbn')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.isbnLocked = true; - this.cdRef.markForCheck(); - }); - - this.editForm.get('ageRating')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.ageRatingLocked = true; - this.cdRef.markForCheck(); - }); - - this.editForm.get('summary')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.summaryLocked = true; - this.cdRef.markForCheck(); - }); - - this.editForm.get('releaseDate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { - this.chapter.releaseDateLocked = true; - this.cdRef.markForCheck(); - }); - - this.setupTypeaheads(); - - } - - - close() { - this.modal.dismiss(); - } - - save() { - const model = this.editForm.value; - const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0; - - // Patch in data from the model that is not typeahead (as those are updated during setting) - if (model.releaseDate === '') { - this.chapter.releaseDate = '0001-01-01T00:00:00'; - } else { - this.chapter.releaseDate = model.releaseDate + 'T00:00:00'; - } - - this.chapter.ageRating = parseInt(model.ageRating + '', 10) as AgeRating; - this.chapter.sortOrder = model.sortOrder; - this.chapter.titleName = model.titleName; - this.chapter.summary = model.summary; - this.chapter.isbn = model.isbn; - - - const apis = [ - this.chapterService.updateChapter(this.chapter) - ]; - - // We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image - const needsReload = this.editForm.get('titleName')?.dirty || this.editForm.get('sortOrder')?.dirty; - - - if (selectedIndex > 0 || this.coverImageReset) { - apis.push(this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover, !this.coverImageReset)); - } - - forkJoin(apis).subscribe(results => { - this.modal.close({success: true, chapter: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: needsReload, isDeleted: false} as EditChapterModalCloseResult); - }); - } - - unlock(b: any, field: string) { - if (b) { - b[field] = !b[field]; - } - this.cdRef.markForCheck(); - } - - async runTask(action: ActionItem) { - switch (action.action) { - - case Action.MarkAsRead: - this.actionService.markChapterAsRead(this.libraryId, this.seriesId, this.chapter, (p) => { - this.chapter.pagesRead = p.pagesRead; - this.cdRef.markForCheck(); - }); - break; - case Action.MarkAsUnread: - this.actionService.markChapterAsUnread(this.libraryId, this.seriesId, this.chapter, (p) => { - this.chapter.pagesRead = 0; - this.cdRef.markForCheck(); - }); - break; - case Action.Delete: - await this.actionService.deleteChapter(this.chapter.id, (b) => { - if (!b) return; - this.modal.close({success: b, chapter: this.chapter, coverImageUpdate: false, needsReload: true, isDeleted: b} as EditChapterModalCloseResult); - }); - break; - case Action.Download: - this.downloadService.download('chapter', this.chapter); - break; - } - } - - setupTypeaheads() { - forkJoin([ - this.setupTagSettings(), - this.setupGenreTypeahead(), - this.setupPersonTypeahead(), - this.setupLanguageTypeahead() - ]).subscribe(results => { - this.cdRef.markForCheck(); - }); - } - - setupTagSettings() { - this.tagsSettings.minCharacters = 0; - this.tagsSettings.multiple = true; - this.tagsSettings.id = 'tags'; - this.tagsSettings.unique = true; - this.tagsSettings.showLocked = true; - this.tagsSettings.addIfNonExisting = true; - - - this.tagsSettings.compareFn = (options: Tag[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.title, filter)); - } - this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags() - .pipe(map(items => this.tagsSettings.compareFn(items, filter))); - - this.tagsSettings.addTransformFn = ((title: string) => { - return {id: 0, title: title }; - }); - this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => { - return a.title.toLowerCase() == b.title.toLowerCase(); - } - this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.title, filter)); - } - this.tagsSettings.trackByIdentityFn = (index, value) => value.title + (value.id + ''); - - if (this.chapter.tags) { - this.tagsSettings.savedData = this.chapter.tags; - } - return of(true); - } - - setupGenreTypeahead() { - this.genreSettings.minCharacters = 0; - this.genreSettings.multiple = true; - this.genreSettings.id = 'genres'; - this.genreSettings.unique = true; - this.genreSettings.showLocked = true; - this.genreSettings.addIfNonExisting = true; - this.genreSettings.fetchFn = (filter: string) => { - return this.metadataService.getAllGenres() - .pipe(map(items => this.genreSettings.compareFn(items, filter))); - }; - this.genreSettings.compareFn = (options: Genre[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.title, filter)); - } - this.genreSettings.compareFnForAdd = (options: Genre[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.title, filter)); - } - this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => { - return a.title.toLowerCase() == b.title.toLowerCase(); - } - - this.genreSettings.addTransformFn = ((title: string) => { - return {id: 0, title: title }; - }); - this.genreSettings.trackByIdentityFn = (index, value) => value.title + (value.id + ''); - - if (this.chapter.genres) { - this.genreSettings.savedData = this.chapter.genres; - } - return of(true); - } - - setupLanguageTypeahead() { - this.languageSettings.minCharacters = 0; - this.languageSettings.multiple = false; - this.languageSettings.id = 'language'; - this.languageSettings.unique = true; - this.languageSettings.showLocked = true; - this.languageSettings.addIfNonExisting = false; - this.languageSettings.compareFn = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.title, filter)); - } - this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.title, filter)); - } - this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) - .pipe(map(items => this.languageSettings.compareFn(items, filter))); - - this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { - return a.isoCode == b.isoCode; - } - this.languageSettings.trackByIdentityFn = (index, value) => value.isoCode; - - const l = this.validLanguages.find(l => l.isoCode === this.chapter.language); - if (l !== undefined) { - this.languageSettings.savedData = l; - } - return of(true); - } - - - updateFromPreset(id: string, presetField: Array | undefined, role: PersonRole) { - const personSettings = this.createBlankPersonSettings(id, role) - - if (presetField && presetField.length > 0) { - const fetch = personSettings.fetchFn as ((filter: string) => Observable); - return fetch('').pipe(map(people => { - const presetIds = presetField.map(p => p.id); - personSettings.savedData = people.filter(person => presetIds.includes(person.id)); - this.peopleSettings[role] = personSettings; - this.metadataService.updatePerson(this.chapter, personSettings.savedData as Person[], role); - this.cdRef.markForCheck(); - return true; - })); - } - - this.peopleSettings[role] = personSettings; - return of(true); - - } - - setupPersonTypeahead() { - this.peopleSettings = {}; - - return forkJoin([ - this.updateFromPreset('writer', this.chapter.writers, PersonRole.Writer), - this.updateFromPreset('character', this.chapter.characters, PersonRole.Character), - this.updateFromPreset('colorist', this.chapter.colorists, PersonRole.Colorist), - this.updateFromPreset('cover-artist', this.chapter.coverArtists, PersonRole.CoverArtist), - this.updateFromPreset('editor', this.chapter.editors, PersonRole.Editor), - this.updateFromPreset('inker', this.chapter.inkers, PersonRole.Inker), - this.updateFromPreset('letterer', this.chapter.letterers, PersonRole.Letterer), - this.updateFromPreset('penciller', this.chapter.pencillers, PersonRole.Penciller), - this.updateFromPreset('publisher', this.chapter.publishers, PersonRole.Publisher), - this.updateFromPreset('imprint', this.chapter.imprints, PersonRole.Imprint), - this.updateFromPreset('translator', this.chapter.translators, PersonRole.Translator), - this.updateFromPreset('teams', this.chapter.teams, PersonRole.Team), - this.updateFromPreset('locations', this.chapter.locations, PersonRole.Location), - ]).pipe(map(_ => { - return of(true); - })); - } - - fetchPeople(role: PersonRole, filter: string) { - return this.metadataService.getAllPeople().pipe(map(people => { - return people.filter(p => this.utilityService.filter(p.name, filter)); - })); - } - - createBlankPersonSettings(id: string, role: PersonRole) { - let personSettings = new TypeaheadSettings(); - personSettings.minCharacters = 0; - personSettings.multiple = true; - personSettings.showLocked = true; - personSettings.unique = true; - personSettings.addIfNonExisting = true; - personSettings.id = id; - personSettings.compareFn = (options: Person[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.name, filter)); - } - personSettings.compareFnForAdd = (options: Person[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.name, filter)); - } - - personSettings.selectionCompareFn = (a: Person, b: Person) => { - return a.name == b.name; - } - personSettings.fetchFn = (filter: string) => { - return this.fetchPeople(role, filter).pipe(map(items => personSettings.compareFn(items, filter))); - }; - - personSettings.addTransformFn = ((title: string) => { - return {id: 0, name: title, aliases: [], role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' }; - }); - - personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + ''); - - return personSettings; - } - - updateTags(tags: Tag[]) { - this.tags = tags; - this.chapter.tags = tags; - this.cdRef.markForCheck(); - } - - updateGenres(genres: Genre[]) { - this.genres = genres; - this.chapter.genres = genres; - this.cdRef.markForCheck(); - } - - updatePerson(persons: Person[], role: PersonRole) { - this.metadataService.updatePerson(this.chapter, persons, role); - this.chapter.locationLocked = true; - this.cdRef.markForCheck(); - } - - updateLanguage(language: Array) { - if (language.length === 0) { - this.chapter.language = ''; - return; - } - this.chapter.language = language[0].isoCode; - this.chapter.languageLocked = true; - this.cdRef.markForCheck(); - } - - updateSelectedIndex(index: number) { - this.editForm.patchValue({ - coverImageIndex: index - }); - this.cdRef.markForCheck(); - } - - updateSelectedImage(url: string) { - this.selectedCover = url; - this.cdRef.markForCheck(); - } - - handleReset() { - this.coverImageReset = true; - this.editForm.patchValue({ - coverImageLocked: false - }); - this.cdRef.markForCheck(); - } - - getPersonsSettings(role: PersonRole) { - return this.peopleSettings[role]; - } -} diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html deleted file mode 100644 index b0f331b51..000000000 --- a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts deleted file mode 100644 index 3407afd83..000000000 --- a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.ts +++ /dev/null @@ -1,203 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {NgbActiveModal, NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {NgClass} from "@angular/common"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component"; -import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; -import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; -import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component"; -import {CompactNumberPipe} from "../../_pipes/compact-number.pipe"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {BytesPipe} from "../../_pipes/bytes.pipe"; -import {ReadTimePipe} from "../../_pipes/read-time.pipe"; -import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service"; -import {Volume} from "../../_models/volume"; -import {Breakpoint, UtilityService} from "../../shared/_services/utility.service"; -import {ImageService} from "../../_services/image.service"; -import {UploadService} from "../../_services/upload.service"; -import {AccountService} from "../../_services/account.service"; -import {ActionService} from "../../_services/action.service"; -import {DownloadService} from "../../shared/_services/download.service"; -import {LibraryType} from "../../_models/library/library"; -import {PersonRole} from "../../_models/metadata/person"; -import {forkJoin} from "rxjs"; -import {MangaFormat} from 'src/app/_models/manga-format'; -import {MangaFile} from "../../_models/manga-file"; -import {VolumeService} from "../../_services/volume.service"; -import {User} from "../../_models/user"; - -enum TabID { - General = 'general-tab', - CoverImage = 'cover-image-tab', - Info = 'info-tab', - Tasks = 'tasks-tab', - Progress = 'progress-tab', -} - -export interface EditVolumeModalCloseResult { - success: boolean; - volume: Volume; - coverImageUpdate: boolean; - needsReload: boolean; - isDeleted: boolean; -} - -const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; - -@Component({ - selector: 'app-edit-volume-modal', - imports: [ - FormsModule, - NgbNav, - NgbNavContent, - NgbNavLink, - TranslocoDirective, - NgbNavOutlet, - ReactiveFormsModule, - NgbNavItem, - SettingItemComponent, - NgClass, - EntityTitleComponent, - SettingButtonComponent, - CoverImageChooserComponent, - EditChapterProgressComponent, - CompactNumberPipe, - DefaultDatePipe, - UtcToLocalTimePipe, - BytesPipe, - ReadTimePipe - ], - templateUrl: './edit-volume-modal.component.html', - styleUrl: './edit-volume-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class EditVolumeModalComponent implements OnInit { - public readonly modal = inject(NgbActiveModal); - public readonly utilityService = inject(UtilityService); - public readonly imageService = inject(ImageService); - private readonly uploadService = inject(UploadService); - private readonly cdRef = inject(ChangeDetectorRef); - public readonly accountService = inject(AccountService); - private readonly actionFactoryService = inject(ActionFactoryService); - private readonly actionService = inject(ActionService); - private readonly downloadService = inject(DownloadService); - private readonly volumeService = inject(VolumeService); - - protected readonly Breakpoint = Breakpoint; - protected readonly TabID = TabID; - protected readonly Action = Action; - protected readonly PersonRole = PersonRole; - protected readonly MangaFormat = MangaFormat; - - @Input({required: true}) volume!: Volume; - @Input({required: true}) libraryType!: LibraryType; - @Input({required: true}) libraryId!: number; - @Input({required: true}) seriesId!: number; - - activeId = TabID.Info; - editForm: FormGroup = new FormGroup({}); - selectedCover: string = ''; - coverImageReset = false; - user!: User; - - - tasks = this.actionFactoryService.getActionablesForSettingsPage(this.actionFactoryService.getVolumeActions(this.runTask.bind(this)), blackList); - /** - * A copy of the chapter from init. This is used to compare values for name fields to see if lock was modified - */ - initVolume!: Volume; - imageUrls: Array = []; - size: number = 0; - files: Array = []; - - constructor() { - this.accountService.currentUser$.subscribe(user => { - this.user = user!; - - if (!this.accountService.hasAdminRole(user!)) { - this.activeId = TabID.Info; - } - this.cdRef.markForCheck(); - }); - } - - - ngOnInit() { - this.initVolume = Object.assign({}, this.volume); - this.imageUrls.push(this.imageService.getVolumeCoverImage(this.volume.id)); - - this.files = this.volume.chapters.flatMap(c => c.files); - this.size = this.files.reduce((sum, v) => sum + v.bytes, 0); - - this.editForm.addControl('coverImageIndex', new FormControl(0, [])); - this.editForm.addControl('coverImageLocked', new FormControl(this.volume.coverImageLocked, [])); - } - - close() { - this.modal.dismiss(); - } - - save() { - const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0; - - const apis = []; - - if (selectedIndex > 0 || this.coverImageReset) { - apis.push(this.uploadService.updateVolumeCoverImage(this.volume.id, this.selectedCover, !this.coverImageReset)); - } - - forkJoin(apis).subscribe(results => { - this.modal.close({success: true, volume: this.volume, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: false, isDeleted: false} as EditVolumeModalCloseResult); - }); - } - - - async runTask(action: ActionItem) { - switch (action.action) { - case Action.MarkAsRead: - this.actionService.markVolumeAsRead(this.seriesId, this.volume, (p) => { - this.volume.pagesRead = p.pagesRead; - this.cdRef.markForCheck(); - }); - break; - case Action.MarkAsUnread: - this.actionService.markVolumeAsUnread(this.seriesId, this.volume, (p) => { - this.volume.pagesRead = 0; - this.cdRef.markForCheck(); - }); - break; - case Action.Delete: - await this.actionService.deleteVolume(this.volume.id, (b) => { - if (!b) return; - this.modal.close({success: b, volume: this.volume, coverImageUpdate: false, needsReload: true, isDeleted: b} as EditVolumeModalCloseResult); - }); - break; - case Action.Download: - this.downloadService.download('volume', this.volume); - break; - } - } - - updateSelectedIndex(index: number) { - this.editForm.patchValue({ - coverImageIndex: index - }); - this.cdRef.markForCheck(); - } - - updateSelectedImage(url: string) { - this.selectedCover = url; - this.cdRef.markForCheck(); - } - - handleReset() { - this.coverImageReset = true; - this.editForm.patchValue({ - coverImageLocked: false - }); - this.cdRef.markForCheck(); - } -} diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html deleted file mode 100644 index 5a9804d54..000000000 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html +++ /dev/null @@ -1,68 +0,0 @@ - -
- - - -
-
- diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.scss b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.scss deleted file mode 100644 index d3a1cb9a9..000000000 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.setting-section-break { - margin: 0 !important; -} \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts deleted file mode 100644 index 793737923..000000000 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {Series} from "../../_models/series"; -import {SeriesService} from "../../_services/series.service"; -import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; -import {translate, TranslocoDirective} from "@jsverse/transloco"; -import {MatchSeriesResultItemComponent} from "../match-series-result-item/match-series-result-item.component"; -import {LoadingComponent} from "../../shared/loading/loading.component"; -import {ExternalSeriesMatch} from "../../_models/series-detail/external-series-match"; -import {ToastrService} from "ngx-toastr"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; -import { ThemeService } from 'src/app/_services/theme.service'; -import { AsyncPipe } from '@angular/common'; - -@Component({ - selector: 'app-match-series-modal', - imports: [ - AsyncPipe, - TranslocoDirective, - MatchSeriesResultItemComponent, - LoadingComponent, - ReactiveFormsModule, - SettingItemComponent, - SettingSwitchComponent - ], - templateUrl: './match-series-modal.component.html', - styleUrl: './match-series-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class MatchSeriesModalComponent implements OnInit { - private readonly cdRef = inject(ChangeDetectorRef); - private readonly seriesService = inject(SeriesService); - private readonly modalService = inject(NgbActiveModal); - private readonly toastr = inject(ToastrService); - protected readonly themeService = inject(ThemeService); - - @Input({required: true}) series!: Series; - - formGroup = new FormGroup({}); - matches: Array = []; - isLoading = true; - - ngOnInit() { - this.formGroup.addControl('query', new FormControl('', [])); - this.formGroup.addControl('dontMatch', new FormControl(this.series?.dontMatch || false, [])); - - this.search(); - } - - search() { - this.isLoading = true; - this.cdRef.markForCheck(); - - const model: any = this.formGroup.value; - model.seriesId = this.series.id; - - if (model.dontMatch) return; - - this.seriesService.matchSeries(model).subscribe(results => { - this.isLoading = false; - this.matches = results; - this.cdRef.markForCheck(); - }); - } - - close() { - this.modalService.close(false); - } - - save() { - - const model: any = this.formGroup.value; - model.seriesId = this.series.id; - - const dontMatchChanged = this.series.dontMatch !== model.dontMatch; - - // We need to update the dontMatch status - if (dontMatchChanged) { - this.seriesService.updateDontMatch(this.series.id, model.dontMatch).subscribe(_ => { - this.modalService.close(true); - }); - } else { - this.toastr.success(translate('toasts.match-success')); - this.modalService.close(true); - } - } - - selectMatch(item: ExternalSeriesMatch) { - const data = item.series; - data.tags = data.tags || []; - data.genres = data.genres || []; - - this.seriesService.updateMatch(this.series.id, item.series).subscribe(_ => { - this.save(); - }); - } - -} diff --git a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html deleted file mode 100644 index 322a16bd8..000000000 --- a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html +++ /dev/null @@ -1,51 +0,0 @@ - -
-
-
- @if (item.series.coverUrl) { - - } -
-
-
{{item.series.name}} ({{item.matchRating | translocoPercent}})
-
- @for(synm of item.series.synonyms; track synm; let last = $last) { - {{synm}} - @if (!last) { - , - } - } -
- @if (item.series.summary) { - - } -
-
- - @if (isSelected) { -
- - {{t('updating-metadata-status')}} -
- } @else { -
- @if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) { - @if (item.series.plusMediaFormat === PlusMediaFormat.Comic) { - {{t('issue-count', {num: item.series.chapters})}} - } @else { - {{t('volume-count', {num: item.series.volumes})}} - {{t('chapter-count', {num: item.series.chapters})}} - } - } @else { - {{t('releasing')}} - } - - {{item.series.plusMediaFormat | plusMediaFormat}} -
- } - -
-
diff --git a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.scss b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.scss deleted file mode 100644 index 5df806397..000000000 --- a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -.search-result { - img { - max-width: 100px; - min-width: 100px; - } -} -.title { - font-size: 1.2rem; - font-weight: bold; - margin: 0; - padding: 0; -} - -.match-item-container { - &.dark { - background-color: var(--elevation-layer6-dark); - } - - &.light { - background-color: var(--elevation-layer6); - } - border-radius: 15px; - - &:hover { - &.dark { - background-color: var(--elevation-layer11-dark); - } - - &.light { - background-color: var(--elevation-layer11); - } - } -} \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.ts b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.ts deleted file mode 100644 index 9e3044884..000000000 --- a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - inject, - Input, - Output -} from '@angular/core'; -import {ImageComponent} from "../../shared/image/image.component"; -import {ExternalSeriesMatch} from "../../_models/series-detail/external-series-match"; -import {TranslocoPercentPipe} from "@jsverse/transloco-locale"; -import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {PlusMediaFormatPipe} from "../../_pipes/plus-media-format.pipe"; -import {LoadingComponent} from "../../shared/loading/loading.component"; -import {PlusMediaFormat} from "../../_models/series-detail/external-series-detail"; - -@Component({ - selector: 'app-match-series-result-item', - imports: [ - ImageComponent, - TranslocoPercentPipe, - ReadMoreComponent, - TranslocoDirective, - PlusMediaFormatPipe, - LoadingComponent - ], - templateUrl: './match-series-result-item.component.html', - styleUrl: './match-series-result-item.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class MatchSeriesResultItemComponent { - - private readonly cdRef = inject(ChangeDetectorRef); - - @Input({required: true}) item!: ExternalSeriesMatch; - @Input({required: true}) isDarkMode = true; - @Output() selected: EventEmitter = new EventEmitter(); - - isSelected = false; - - selectItem() { - if (this.isSelected) return; - - this.isSelected = true; - this.cdRef.markForCheck(); - this.selected.emit(this.item); - } - - protected readonly PlusMediaFormat = PlusMediaFormat; -} diff --git a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.html b/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.html deleted file mode 100644 index 00b625413..000000000 --- a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.html +++ /dev/null @@ -1,39 +0,0 @@ -@if (publishers.length > 0) { -
-
-
-
- -
- {{currentPublisher!.name}} -
-
-
-
-
- -
- {{nextPublisher!.name}} -
-
-
-
-
-} - diff --git a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.scss b/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.scss deleted file mode 100644 index 9f4486d16..000000000 --- a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.scss +++ /dev/null @@ -1,59 +0,0 @@ -// -//.publisher-img-container { -// background-color: var(--card-bg-color); -// border-radius: 3px; -// padding: 2px 5px; -// font-size: 0.8rem; -// vertical-align: middle; -// -// div { -// min-height: 32px; -// line-height: 32px; -// } -//} - -// Animation code - -.publisher-wrapper { - perspective: 1000px; - height: 32px; - - background-color: var(--card-bg-color); - border-radius: 3px; - padding: 2px 5px; - font-size: 0.8rem; - vertical-align: middle; - - div { - min-height: 32px; - line-height: 32px; - } -} - -.publisher-flipper { - position: relative; - width: 100%; - height: 100%; - text-align: left; - transition: transform 0.6s ease; - transform-style: preserve-3d; -} - -.publisher-flipper.is-flipped { - transform: rotateX(180deg); -} - -.publisher-side { - position: absolute; - width: 100%; - height: 100%; - backface-visibility: hidden; -} - -.publisher-front { - z-index: 2; -} - -.publisher-back { - transform: rotateX(180deg); -} diff --git a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.ts b/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.ts deleted file mode 100644 index fe54cdaa1..000000000 --- a/UI/Web/src/app/_single-module/publisher-flipper/publisher-flipper.component.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - AfterViewChecked, - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - inject, - Input, - OnDestroy, - OnInit -} from '@angular/core'; -import {ImageComponent} from "../../shared/image/image.component"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; -import {Person} from "../../_models/metadata/person"; -import {ImageService} from "../../_services/image.service"; -import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; -import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service"; -import {Router} from "@angular/router"; - -const ANIMATION_TIME = 3000; - -@Component({ - selector: 'app-publisher-flipper', - imports: [ - ImageComponent - ], - templateUrl: './publisher-flipper.component.html', - styleUrl: './publisher-flipper.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class PublisherFlipperComponent implements OnInit, OnDestroy, AfterViewInit, AfterViewChecked { - - protected readonly imageService = inject(ImageService); - private readonly filterUtilityService = inject(FilterUtilitiesService); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly router = inject(Router); - - @Input() publishers: Array = []; - - - currentPublisher: Person | undefined = undefined; - nextPublisher: Person | undefined = undefined; - - currentIndex = 0; - isFlipped = false; - private intervalId: any; - - ngOnInit() { - if (this.publishers.length > 0) { - this.currentPublisher = this.publishers[0]; - this.nextPublisher = this.publishers[1] || this.publishers[0]; - } - } - - ngAfterViewInit() { - if (this.publishers.length > 1) { - this.startFlipping(); // Start flipping cycle once the view is initialized - } - } - - ngAfterViewChecked() { - // This lifecycle hook will be called after Angular performs change detection in each cycle - if (this.isFlipped) { - // Only update publishers after the flip is complete - this.currentIndex = (this.currentIndex + 1) % this.publishers.length; - this.currentPublisher = this.publishers[this.currentIndex]; - this.nextPublisher = this.publishers[(this.currentIndex + 1) % this.publishers.length]; - } - } - - ngOnDestroy() { - if (this.intervalId) { - clearInterval(this.intervalId); - } - } - - private startFlipping() { - this.intervalId = setInterval(() => { - // Toggle flip state, initiating the flip animation - this.isFlipped = !this.isFlipped; - this.cdRef.detectChanges(); // Explicitly detect changes to trigger re-render - }, ANIMATION_TIME); - } - - openPublisher(filter: string | number) { - // TODO: once we build out publisher person-detail page, we can redirect there - this.filterUtilityService.applyFilter(['all-series'], FilterField.Publisher, FilterComparison.Equal, `${filter}`).subscribe(); - } -} diff --git a/UI/Web/src/app/_single-module/related-tab/related-tab.component.html b/UI/Web/src/app/_single-module/related-tab/related-tab.component.html deleted file mode 100644 index 8334eaf21..000000000 --- a/UI/Web/src/app/_single-module/related-tab/related-tab.component.html +++ /dev/null @@ -1,47 +0,0 @@ - -
- @if (relations.length > 0) { - - - - - - } - - @if (collections.length > 0) { - - - - - - } - - - @if (readingLists.length > 0) { - - - - - - } - - @if (bookmarks.length > 0) { - - - - - - } -
-
diff --git a/UI/Web/src/app/_single-module/related-tab/related-tab.component.ts b/UI/Web/src/app/_single-module/related-tab/related-tab.component.ts deleted file mode 100644 index 8d8a767d5..000000000 --- a/UI/Web/src/app/_single-module/related-tab/related-tab.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {ChangeDetectionStrategy, Component, inject, Input, OnInit} from '@angular/core'; -import {ReadingList} from "../../_models/reading-list"; -import {CardItemComponent} from "../../cards/card-item/card-item.component"; -import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component"; -import {ImageService} from "../../_services/image.service"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {UserCollection} from "../../_models/collection-tag"; -import {Router} from "@angular/router"; -import {SeriesCardComponent} from "../../cards/series-card/series-card.component"; -import {Series} from "../../_models/series"; -import {RelationKind} from "../../_models/series-detail/relation-kind"; -import {PageBookmark} from "../../_models/readers/page-bookmark"; - -export interface RelatedSeriesPair { - series: Series; - relation: RelationKind; -} - -@Component({ - selector: 'app-related-tab', - imports: [ - CardItemComponent, - CarouselReelComponent, - TranslocoDirective, - SeriesCardComponent - ], - templateUrl: './related-tab.component.html', - styleUrl: './related-tab.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class RelatedTabComponent { - - protected readonly imageService = inject(ImageService); - protected readonly router = inject(Router); - - @Input() readingLists: Array = []; - @Input() collections: Array = []; - @Input() relations: Array = []; - @Input() bookmarks: Array = []; - @Input() libraryId!: number; - - openReadingList(readingList: ReadingList) { - this.router.navigate(['lists', readingList.id]); - } - - openCollection(collection: UserCollection) { - this.router.navigate(['collections', collection.id]); - } - - viewBookmark(bookmark: PageBookmark) { - this.router.navigate(['library', this.libraryId, 'series', bookmark.seriesId, 'manga', 0], {queryParams: {incognitoMode: false, bookmarkMode: true}}); - } - -} diff --git a/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.html b/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.html index 773817c90..177e63d11 100644 --- a/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.html +++ b/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.html @@ -1,26 +1,17 @@
diff --git a/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.ts b/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.ts index 99ab421a5..1f0d521fb 100644 --- a/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.ts +++ b/UI/Web/src/app/_single-module/review-card-modal/review-card-modal.component.ts @@ -2,39 +2,36 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - inject, Inject, - Input, - ViewChild, + Input, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; -import {DOCUMENT, NgOptimizedImage} from '@angular/common'; +import {CommonModule, DOCUMENT} from '@angular/common'; import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; import {ReactiveFormsModule} from "@angular/forms"; import {UserReview} from "../review-card/user-review"; import {SpoilerComponent} from "../spoiler/spoiler.component"; -import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {ProviderImagePipe} from "../../_pipes/provider-image.pipe"; +import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; +import {TranslocoDirective} from "@ngneat/transloco"; @Component({ selector: 'app-review-card-modal', - imports: [ReactiveFormsModule, SafeHtmlPipe, TranslocoDirective, NgOptimizedImage, ProviderImagePipe], + standalone: true, + imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective], templateUrl: './review-card-modal.component.html', styleUrls: ['./review-card-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, }) export class ReviewCardModalComponent implements AfterViewInit { - private modal = inject(NgbActiveModal); - @Input({required: true}) review!: UserReview; @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef; - constructor(@Inject(DOCUMENT) private document: Document) {} + constructor(private modal: NgbActiveModal, @Inject(DOCUMENT) private document: Document) { + } close() { this.modal.close(); @@ -45,8 +42,7 @@ export class ReviewCardModalComponent implements AfterViewInit { for (let i = 0; i < spoilers.length; i++) { const spoiler = spoilers[i]; - const componentRef = this.container.createComponent(SpoilerComponent, - {projectableNodes: [[document.createTextNode('')]]}); + const componentRef = this.container.createComponent(SpoilerComponent); componentRef.instance.html = spoiler.innerHTML; if (spoiler.parentNode != null) { spoiler.parentNode.replaceChild(componentRef.location.nativeElement, spoiler); diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.html b/UI/Web/src/app/_single-module/review-card/review-card.component.html index c5bade722..41722fb89 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.html +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.html @@ -1,39 +1,38 @@ -
+
-
- @if (isMyReview) { - - - } @else { - - } +
+ +
+ + {{t('your-review')}} +
-
-
- +
+
+
+ {{review.tagline.substring(0, 29)}}{{review.tagline.length > 29 ? '…' : ''}} + + {{review.isExternal ? t('external-review') : t('local-review')}} + +

- +

- diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.scss b/UI/Web/src/app/_single-module/review-card/review-card.component.scss index 7859ba11c..bb425ec08 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.scss +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.scss @@ -1,12 +1,5 @@ -.review-card { - max-width: 320px; - max-height: 130px; - height: 130px; - width: 320px; -} - .profile-image { - font-size: 1.2rem; + font-size: 2rem; padding: 20px; } @@ -33,10 +26,16 @@ } .card-text.no-images { + min-height: 63px; + max-height: 63px; text-overflow: ellipsis; overflow: hidden; } +.card { + cursor: pointer; +} + .no-images img { display: none; } @@ -44,19 +43,7 @@ .card-footer { font-size: 13px; display: flex; - max-width: 319px; + max-width: 305px; justify-content: space-between; margin: 0 auto; - - & > * { - margin: 0 5px; - display: inline-flex; - } } - -.card-body { - display: block; - visibility: visible; - min-height: 93.5px; - max-height: 93.5px; -} \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.ts b/UI/Web/src/app/_single-module/review-card/review-card.component.ts index f86bca966..ff9b03848 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.ts +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.ts @@ -1,40 +1,28 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - inject, - Input, - OnInit, - Output -} from '@angular/core'; -import {NgOptimizedImage} from '@angular/common'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import {CommonModule, NgOptimizedImage} from '@angular/common'; import {UserReview} from "./user-review"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component"; import {AccountService} from "../../_services/account.service"; -import {ReviewModalCloseEvent, ReviewModalComponent} from "../review-modal/review-modal.component"; +import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component"; import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; -import {ProviderImagePipe} from "../../_pipes/provider-image.pipe"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {ScrobbleProvider} from "../../_services/scrobbling.service"; -import {RatingAuthority} from "../../_models/rating"; +import {DefaultValuePipe} from "../../pipe/default-value.pipe"; +import {ImageComponent} from "../../shared/image/image.component"; +import {ProviderImagePipe} from "../../pipe/provider-image.pipe"; +import {TranslocoDirective} from "@ngneat/transloco"; @Component({ selector: 'app-review-card', - imports: [ReadMoreComponent, DefaultValuePipe, NgOptimizedImage, ProviderImagePipe, TranslocoDirective], + standalone: true, + imports: [CommonModule, ReadMoreComponent, DefaultValuePipe, ImageComponent, NgOptimizedImage, ProviderImagePipe, TranslocoDirective], templateUrl: './review-card.component.html', styleUrls: ['./review-card.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ReviewCardComponent implements OnInit { - private readonly accountService = inject(AccountService); - protected readonly ScrobbleProvider = ScrobbleProvider; @Input({required: true}) review!: UserReview; - @Output() refresh = new EventEmitter(); - + private readonly accountService = inject(AccountService); isMyReview: boolean = false; constructor(private readonly modalService: NgbModal, private readonly cdRef: ChangeDetectorRef) {} @@ -42,7 +30,7 @@ export class ReviewCardComponent implements OnInit { ngOnInit() { this.accountService.currentUser$.subscribe(u => { if (u) { - this.isMyReview = this.review.username === u.username && !this.review.isExternal; + this.isMyReview = this.review.username === u.username; this.cdRef.markForCheck(); } }); @@ -51,19 +39,12 @@ export class ReviewCardComponent implements OnInit { showModal() { let component; if (this.isMyReview) { - component = ReviewModalComponent; + component = ReviewSeriesModalComponent; } else { component = ReviewCardModalComponent; } - const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'}); - + const ref = this.modalService.open(component, {size: "lg"}); ref.componentInstance.review = this.review; - ref.closed.subscribe((res: ReviewModalCloseEvent | undefined) => { - if (res) { - this.refresh.emit(res); - } - }) } - protected readonly RatingAuthority = RatingAuthority; } diff --git a/UI/Web/src/app/_single-module/review-card/user-review.ts b/UI/Web/src/app/_single-module/review-card/user-review.ts index 58af94dea..f735d9548 100644 --- a/UI/Web/src/app/_single-module/review-card/user-review.ts +++ b/UI/Web/src/app/_single-module/review-card/user-review.ts @@ -1,18 +1,14 @@ import {ScrobbleProvider} from "../../_services/scrobbling.service"; -import {RatingAuthority} from "../../_models/rating"; - export interface UserReview { seriesId: number; libraryId: number; - chapterId?: number; score: number; username: string; body: string; tagline?: string; isExternal: boolean; bodyJustText?: string; - siteUrl?: string; + externalUrl?: string; provider: ScrobbleProvider; - authority: RatingAuthority; } diff --git a/UI/Web/src/app/_single-module/review-modal/review-modal.component.html b/UI/Web/src/app/_single-module/review-modal/review-modal.component.html deleted file mode 100644 index 582a538c3..000000000 --- a/UI/Web/src/app/_single-module/review-modal/review-modal.component.html +++ /dev/null @@ -1,43 +0,0 @@ - -
- - - -
-
- - diff --git a/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts b/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts deleted file mode 100644 index 2470f1679..000000000 --- a/UI/Web/src/app/_single-module/review-modal/review-modal.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; -import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import {SeriesService} from 'src/app/_services/series.service'; -import {UserReview} from "../review-card/user-review"; -import {translate, TranslocoDirective} from "@jsverse/transloco"; -import {ConfirmService} from "../../shared/confirm.service"; -import {ToastrService} from "ngx-toastr"; -import {ChapterService} from "../../_services/chapter.service"; -import {of} from "rxjs"; -import {NgxStarsModule} from "ngx-stars"; -import {ThemeService} from "../../_services/theme.service"; -import {ReviewService} from "../../_services/review.service"; - -export enum ReviewModalCloseAction { - Create, - Edit, - Delete, - Close -} -export interface ReviewModalCloseEvent { - success: boolean, - review: UserReview; - action: ReviewModalCloseAction -} - -@Component({ - selector: 'app-review-series-modal', - imports: [ReactiveFormsModule, TranslocoDirective, NgxStarsModule], - templateUrl: './review-modal.component.html', - styleUrls: ['./review-modal.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ReviewModalComponent implements OnInit { - - protected readonly modal = inject(NgbActiveModal); - private readonly reviewService = inject(ReviewService); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly confirmService = inject(ConfirmService); - private readonly toastr = inject(ToastrService); - protected readonly minLength = 5; - - @Input({required: true}) review!: UserReview; - reviewGroup!: FormGroup; - - ngOnInit(): void { - this.reviewGroup = new FormGroup({ - reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]), - }); - this.cdRef.markForCheck(); - } - - close() { - this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close}); - } - - async delete() { - if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return; - - this.reviewService.deleteReview(this.review.seriesId, this.review.chapterId).subscribe(() => { - this.toastr.success(translate('toasts.review-deleted')); - this.modal.close({success: true, review: this.review, action: ReviewModalCloseAction.Delete}); - }); - - } - save() { - const model = this.reviewGroup.value; - if (model.reviewBody.length < this.minLength) { - return; - } - - this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.review.chapterId).subscribe(review => { - this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit}); - }); - - } -} diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html new file mode 100644 index 000000000..00c60cc80 --- /dev/null +++ b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html @@ -0,0 +1,30 @@ + +
+ + + +
+
+ + diff --git a/UI/Web/src/app/user-settings/manage-scrobbling-providers/manage-scrobbling-providers.component.scss b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.scss similarity index 100% rename from UI/Web/src/app/user-settings/manage-scrobbling-providers/manage-scrobbling-providers.component.scss rename to UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.scss diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts new file mode 100644 index 000000000..bafbe9ce8 --- /dev/null +++ b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts @@ -0,0 +1,42 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap'; +import { SeriesService } from 'src/app/_services/series.service'; +import {UserReview} from "../review-card/user-review"; +import {CommonModule} from "@angular/common"; +import {TranslocoDirective} from "@ngneat/transloco"; + +@Component({ + selector: 'app-review-series-modal', + standalone: true, + imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective], + templateUrl: './review-series-modal.component.html', + styleUrls: ['./review-series-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ReviewSeriesModalComponent implements OnInit { + + @Input({required: true}) review!: UserReview; + reviewGroup!: FormGroup; + + constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.reviewGroup = new FormGroup({ + tagline: new FormControl(this.review.tagline || '', [Validators.min(20), Validators.max(120)]), + reviewBody: new FormControl(this.review.body, [Validators.min(20)]), + }); + this.cdRef.markForCheck(); + } + + close() { + this.modal.close({success: false, review: null}); + } + + save() { + const model = this.reviewGroup.value; + this.seriesService.updateReview(this.review.seriesId, model.tagline, model.reviewBody).subscribe(() => { + this.modal.close({success: true}); + }); + } +} diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.html b/UI/Web/src/app/_single-module/reviews/reviews.component.html deleted file mode 100644 index 6a8c5c3af..000000000 --- a/UI/Web/src/app/_single-module/reviews/reviews.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
- - - - - -
- -
- - - - - -
diff --git a/UI/Web/src/app/_single-module/reviews/reviews.component.ts b/UI/Web/src/app/_single-module/reviews/reviews.component.ts deleted file mode 100644 index 6e1548876..000000000 --- a/UI/Web/src/app/_single-module/reviews/reviews.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnInit} from '@angular/core'; -import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component"; -import {ReviewCardComponent} from "../review-card/review-card.component"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {UserReview} from "../review-card/user-review"; -import {User} from "../../_models/user"; -import {AccountService} from "../../_services/account.service"; -import { - ReviewModalComponent, ReviewModalCloseAction, - ReviewModalCloseEvent -} from "../review-modal/review-modal.component"; -import {DefaultModalOptions} from "../../_models/default-modal-options"; -import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; -import {Series} from "../../_models/series"; -import {Volume} from "../../_models/volume"; -import {Chapter} from "../../_models/chapter"; - -@Component({ - selector: 'app-reviews', - imports: [ - CarouselReelComponent, - ReviewCardComponent, - TranslocoDirective - ], - templateUrl: './reviews.component.html', - styleUrl: './reviews.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ReviewsComponent { - - @Input({required: true}) userReviews!: Array; - @Input({required: true}) plusReviews!: Array; - @Input({required: true}) series!: Series; - @Input() volumeId: number | undefined; - @Input() chapter: Chapter | undefined; - - user: User | undefined; - - constructor( - private accountService: AccountService, - private modalService: NgbModal, - private cdRef: ChangeDetectorRef) { - - this.accountService.currentUser$.subscribe(user => { - if (user) { - this.user = user; - } - }); - } - - openReviewModal() { - const userReview = this.getUserReviews(); - - const modalRef = this.modalService.open(ReviewModalComponent, DefaultModalOptions); - - if (userReview.length > 0) { - modalRef.componentInstance.review = userReview[0]; - } else { - modalRef.componentInstance.review = { - seriesId: this.series.id, - volumeId: this.volumeId, - chapterId: this.chapter?.id, - tagline: '', - body: '' - }; - } - - modalRef.closed.subscribe((closeResult) => { - this.updateOrDeleteReview(closeResult); - }); - - } - - updateOrDeleteReview(closeResult: ReviewModalCloseEvent) { - if (closeResult.action === ReviewModalCloseAction.Close) return; - - const index = this.userReviews.findIndex(r => r.username === closeResult.review!.username); - if (closeResult.action === ReviewModalCloseAction.Edit) { - if (index === -1 ) { - this.userReviews = [closeResult.review, ...this.userReviews]; - this.cdRef.markForCheck(); - return; - } - this.userReviews[index] = closeResult.review; - this.cdRef.markForCheck(); - return; - } - - if (closeResult.action === ReviewModalCloseAction.Delete) { - this.userReviews = [...this.userReviews.filter(r => r.username !== closeResult.review!.username)]; - this.cdRef.markForCheck(); - return; - } - } - - getUserReviews() { - if (!this.user) { - return []; - } - return this.userReviews.filter(r => r.username === this.user?.username && !r.isExternal); - } - -} diff --git a/UI/Web/src/app/_pipes/scrobble-event-type.pipe.ts b/UI/Web/src/app/_single-module/scrobble-event-type.pipe.ts similarity index 95% rename from UI/Web/src/app/_pipes/scrobble-event-type.pipe.ts rename to UI/Web/src/app/_single-module/scrobble-event-type.pipe.ts index 7597b7f38..08e0b2996 100644 --- a/UI/Web/src/app/_pipes/scrobble-event-type.pipe.ts +++ b/UI/Web/src/app/_single-module/scrobble-event-type.pipe.ts @@ -1,6 +1,6 @@ import {inject, Pipe, PipeTransform} from '@angular/core'; import {ScrobbleEventType} from "../_models/scrobbling/scrobble-event"; -import {TranslocoService} from "@jsverse/transloco"; +import {TranslocoService} from "@ngneat/transloco"; @Pipe({ name: 'scrobbleEventType', diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html index e9be86cce..618eba9fc 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.html @@ -7,42 +7,24 @@
- @if (CoverUrl; as coverUrl) { +
- @if (coverUrl) { - - } +
- } +
- @if (externalSeries) { - @if ((externalSeries.volumeCount || 0) > 0 || (externalSeries.chapterCount || 0) > 0) { -
- {{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}} -
- } + +
+ {{t('series-preview-drawer.vols-and-chapters', {volCount: externalSeries.volumeCount, chpCount: externalSeries.chapterCount})}} +
+ - @if(isExternalSeries && externalSeries) { -
- {{t('series-preview-drawer.provided-by-label')}} - -
- } - - @if (externalSeries.summary) { - - } - } - - - {{t('series-preview-drawer.view-series')}} - - - @if (externalSeries) {
- {{item}} + + {{item}} +
@@ -50,70 +32,25 @@
- {{item.name}} + + {{item.name}} +
- + -
-
-
- @if (item.imageUrl && !item.imageUrl.endsWith('default.jpg')) { - - } @else { - - } -
-
-
-
{{item.name}}
-

{{item.role}}

-
-
-
-
-
-
-
- } - @else if(localSeries) { -
- {{localSeries.publicationStatus | publicationStatus}} - -
- - - -
- - - {{item.title}} - - -
- -
- - - {{item.title}} - - -
- -
- - -
+
- + + + + + +
@@ -126,8 +63,67 @@
- } + + + + +
+ {{localSeries.publicationStatus | publicationStatus}} + +
+ + +
+ + + + {{item.title}} + + + +
+ +
+ + + + {{item.title}} + + + +
+ +
+ + +
+
+
+ +
+
+
+
{{item.name}}
+

{{item.role}}

+
+
+
+
+
+
+
+ +
+
+ + + {{t('series-preview-drawer.view-series')}} +
diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss index d3c04eff2..c34531887 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.scss @@ -15,13 +15,4 @@ a.read-more-link { white-space: nowrap; -} - -.not-clickable { - cursor: text; -} - -.offcanvas-body { - mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%); -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts index 46afe7f17..1e6a420b0 100644 --- a/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts +++ b/UI/Web/src/app/_single-module/series-preview-drawer/series-preview-drawer.component.ts @@ -1,38 +1,31 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {NgOptimizedImage} from '@angular/common'; -import {TranslocoDirective} from "@jsverse/transloco"; +import {CommonModule} from '@angular/common'; +import {TranslocoDirective} from "@ngneat/transloco"; import {NgbActiveOffcanvas, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/external-series-detail"; import {SeriesService} from "../../_services/series.service"; import {ImageComponent} from "../../shared/image/image.component"; import {LoadingComponent} from "../../shared/loading/loading.component"; +import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; +import {A11yClickDirective} from "../../shared/a11y-click.directive"; import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component"; +import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component"; +import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component"; import {ImageService} from "../../_services/image.service"; -import {PublicationStatusPipe} from "../../_pipes/publication-status.pipe"; +import {PublicationStatusPipe} from "../../pipe/publication-status.pipe"; import {SeriesMetadata} from "../../_models/metadata/series-metadata"; import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; import {ActionService} from "../../_services/action.service"; -import {ProviderImagePipe} from "../../_pipes/provider-image.pipe"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; @Component({ - selector: 'app-series-preview-drawer', - imports: [TranslocoDirective, ImageComponent, LoadingComponent, MetadataDetailComponent, - PublicationStatusPipe, ReadMoreComponent, NgbTooltip, NgOptimizedImage, ProviderImagePipe], - templateUrl: './series-preview-drawer.component.html', - styleUrls: ['./series-preview-drawer.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-series-preview-drawer', + standalone: true, + imports: [CommonModule, TranslocoDirective, ImageComponent, LoadingComponent, SafeHtmlPipe, A11yClickDirective, MetadataDetailComponent, PersonBadgeComponent, TagBadgeComponent, PublicationStatusPipe, ReadMoreComponent, NgbTooltip], + templateUrl: './series-preview-drawer.component.html', + styleUrls: ['./series-preview-drawer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class SeriesPreviewDrawerComponent implements OnInit { - - private readonly activeOffcanvas = inject(NgbActiveOffcanvas); - private readonly seriesService = inject(SeriesService); - private readonly imageService = inject(ImageService); - private readonly actionService = inject(ActionService); - private readonly cdRef = inject(ChangeDetectorRef); - - protected readonly FilterField = FilterField; - @Input({required: true}) name!: string; @Input() aniListId?: number; @Input() malId?: number; @@ -47,7 +40,11 @@ export class SeriesPreviewDrawerComponent implements OnInit { url: string = ''; wantToRead: boolean = false; - + private readonly activeOffcanvas = inject(NgbActiveOffcanvas); + private readonly seriesService = inject(SeriesService); + private readonly imageService = inject(ImageService); + private readonly actionService = inject(ActionService); + private readonly cdRef = inject(ChangeDetectorRef); get CoverUrl() { if (this.isExternalSeries) { @@ -62,6 +59,7 @@ export class SeriesPreviewDrawerComponent implements OnInit { if (this.isExternalSeries) { this.seriesService.getExternalSeriesDetails(this.aniListId, this.malId).subscribe(externalSeries => { this.externalSeries = externalSeries; + this.isLoading = false; if (this.externalSeries.siteUrl) { this.url = this.externalSeries.siteUrl; diff --git a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.html b/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.html deleted file mode 100644 index 525155e6a..000000000 --- a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.html +++ /dev/null @@ -1,40 +0,0 @@ - -
-
- {{collection.title}} -
- -
- -
- -
- - - {{collection.lastSyncUtc | utcToLocalTime:'shortDate' | defaultDate}} - - -
- - -
- - - {{collection.totalSourceCount - series.length}} / {{collection.totalSourceCount | number}} - - - - @if(collection.missingSeriesFromSource) { -

- } - @for(s of series; track s.name) { - - } -
-
-
-
-
diff --git a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.scss b/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.scss deleted file mode 100644 index 55f5cfd90..000000000 --- a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -:host { - height: 100%; - display: flex; - flex-direction: column; -} diff --git a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.ts b/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.ts deleted file mode 100644 index ec67ecf7a..000000000 --- a/UI/Web/src/app/_single-module/smart-collection-drawer/smart-collection-drawer.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; -import {NgbActiveOffcanvas} from "@ng-bootstrap/ng-bootstrap"; -import {UserCollection} from "../../_models/collection-tag"; -import {DecimalPipe} from "@angular/common"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {Series} from "../../_models/series"; -import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {RouterLink} from "@angular/router"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; - -@Component({ - selector: 'app-smart-collection-drawer', - imports: [ - TranslocoDirective, - SafeHtmlPipe, - RouterLink, - DefaultDatePipe, - UtcToLocalTimePipe, - SettingItemComponent, - DecimalPipe - ], - templateUrl: './smart-collection-drawer.component.html', - styleUrl: './smart-collection-drawer.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SmartCollectionDrawerComponent implements OnInit { - private readonly activeOffcanvas = inject(NgbActiveOffcanvas); - private readonly cdRef = inject(ChangeDetectorRef); - - @Input({required: true}) collection!: UserCollection; - @Input({required: true}) series: Series[] = []; - - ngOnInit() { - - } - - close() { - this.activeOffcanvas.close(); - } -} diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.html b/UI/Web/src/app/_single-module/sort-button/sort-button.component.html deleted file mode 100644 index bc02c743d..000000000 --- a/UI/Web/src/app/_single-module/sort-button/sort-button.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts b/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts deleted file mode 100644 index 230a0ee6f..000000000 --- a/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {ChangeDetectionStrategy, Component, input, model} from '@angular/core'; -import {TranslocoDirective} from "@jsverse/transloco"; - -@Component({ - selector: 'app-sort-button', - imports: [ - TranslocoDirective - ], - templateUrl: './sort-button.component.html', - styleUrl: './sort-button.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SortButtonComponent { - - disabled = input(false); - isAscending = model(true); - - updateSortOrder() { - this.isAscending.set(!this.isAscending()); - } -} diff --git a/UI/Web/src/app/_single-module/spoiler/spoiler.component.html b/UI/Web/src/app/_single-module/spoiler/spoiler.component.html index c8a4ddc53..67b6e2a5e 100644 --- a/UI/Web/src/app/_single-module/spoiler/spoiler.component.html +++ b/UI/Web/src/app/_single-module/spoiler/spoiler.component.html @@ -1,9 +1,8 @@
- @if (isCollapsed) { - {{t('click-to-show')}} - } @else { + {{t('click-to-show')}} +
- } +
diff --git a/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss b/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss index e2b2c98a9..e3559f7af 100644 --- a/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss +++ b/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss @@ -2,5 +2,6 @@ background-color: var(--review-spoiler-bg-color); color: var(--review-spoiler-text-color); cursor: pointer; + } diff --git a/UI/Web/src/app/_single-module/spoiler/spoiler.component.ts b/UI/Web/src/app/_single-module/spoiler/spoiler.component.ts index 4c5fc1982..4dcbfc82d 100644 --- a/UI/Web/src/app/_single-module/spoiler/spoiler.component.ts +++ b/UI/Web/src/app/_single-module/spoiler/spoiler.component.ts @@ -7,16 +7,18 @@ import { OnInit, ViewEncapsulation } from '@angular/core'; -import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {TranslocoDirective} from "@jsverse/transloco"; +import {CommonModule} from '@angular/common'; +import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; +import {TranslocoDirective} from "@ngneat/transloco"; @Component({ - selector: 'app-spoiler', - imports: [SafeHtmlPipe, TranslocoDirective], - templateUrl: './spoiler.component.html', - styleUrls: ['./spoiler.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None + selector: 'app-spoiler', + standalone: true, + imports: [CommonModule, SafeHtmlPipe, TranslocoDirective], + templateUrl: './spoiler.component.html', + styleUrls: ['./spoiler.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None }) export class SpoilerComponent implements OnInit{ diff --git a/UI/Web/src/app/_single-module/table/_directives/sortable-header.directive.ts b/UI/Web/src/app/_single-module/table/_directives/sortable-header.directive.ts index 3f5d880d6..20df49758 100644 --- a/UI/Web/src/app/_single-module/table/_directives/sortable-header.directive.ts +++ b/UI/Web/src/app/_single-module/table/_directives/sortable-header.directive.ts @@ -1,4 +1,4 @@ -import {ChangeDetectorRef, Directive, EventEmitter, inject, Input, OnInit, Output} from "@angular/core"; +import { Directive, EventEmitter, Input, Output } from "@angular/core"; export const compare = (v1: string | number, v2: string | number) => (v1 < v2 ? -1 : v1 > v2 ? 1 : 0); export type SortColumn = keyof T | ''; @@ -11,7 +11,6 @@ export interface SortEvent { } @Directive({ - // eslint-disable-next-line @angular-eslint/directive-selector selector: 'th[sortable]', host: { '[class.asc]': 'direction === "asc"', @@ -30,4 +29,4 @@ export class SortableHeader { this.direction = rotate[this.direction]; this.sort.emit({ column: this.sortable, direction: this.direction }); } -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html index f5f4e1e26..9e5b01795 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html @@ -1,140 +1,85 @@ - - @let currentUser = accountService.currentUser$ | async; - -
- -
- - @if (tokenExpired) { -

{{t('token-expired')}}

- } @else if (!currentUser!.preferences.aniListScrobblingEnabled) { -

{{t('scrobbling-disabled')}}

- } - +
{{t('title')}}

{{t('description')}}

-

{{t('not-read-warning')}}

-
-
- -
- - +
+ +
+ +
-
- + +
+
+ +
- - - - - -
- - -
-
- - - -
- - - + + + + + + + + + + + + + + + + + + + + + + + + +
{{t('created-header')}} - - - {{value | utcToLocalTime | defaultValue }} - - - - - + + {{t('last-modified-header')}} + {{t('type-header')}} - - - {{value | scrobbleEventType}} - - - - - + {{t('series-header')}} - - - {{item.seriesName}} - - - - - + {{t('data-header')}} - - - @switch (item.scrobbleEventType) { - @case (ScrobbleEventType.ChapterRead) { - @if(item.volumeNumber === LooseLeafOrDefaultNumber) { - @if (item.chapterNumber === LooseLeafOrDefaultNumber) { - {{t('special')}} - } @else { - {{t('chapter-num', {num: item.chapterNumber})}} - } - } - @else if (item.chapterNumber === LooseLeafOrDefaultNumber) { - {{t('volume-num', {num: item.volumeNumber})}} - } - @else if (item.chapterNumber === LooseLeafOrDefaultNumber && item.volumeNumber === SpecialVolumeNumber) { - Special - } - @else { - {{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}} - } - } - @case (ScrobbleEventType.ScoreUpdated) { - {{t('rating', {r: item.rating})}} - } - @default { - {{t('not-applicable')}} - } - } - - - - - + {{t('is-processed-header')}} - - - @if(item.isProcessed) { - - } @else if (item.isErrored) { - - } @else { - - } +
{{t('no-data')}}
+ {{item.createdUtc | utcToLocalTime | defaultValue}} + + {{item.lastModifiedUtc | utcToLocalTime | defaultValue }} + + {{item.scrobbleEventType | scrobbleEventType}} + + {{item.seriesName}} + + + + {{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}} + + + {{t('rating', {r: item.rating})}} + + + {{t('not-applicable')}} + + + + + - {{item.isProcessed ? t('processed') : t('not-processed')}} - - - - - + {{item.isProcessed ? t('processed') : t('not-processed')}} + +
diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss index bf691441b..d1e48851e 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss @@ -1,12 +1,3 @@ .icon { color: var(--primary-color); } - -.error { - color: var(--error-color); -} - -.custom-position { - right: 15px; - top: -42px; -} diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts index ac48b6add..166e00254 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts @@ -1,151 +1,83 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - HostListener, - inject, - OnInit -} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; -import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service"; +import {ScrobblingService} from "../../_services/scrobbling.service"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ScrobbleEvent, ScrobbleEventType} from "../../_models/scrobbling/scrobble-event"; -import {ScrobbleEventTypePipe} from "../../_pipes/scrobble-event-type.pipe"; -import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {ScrobbleEventTypePipe} from "../scrobble-event-type.pipe"; +import {NgbPagination} from "@ng-bootstrap/ng-bootstrap"; import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-filter"; import {debounceTime, take} from "rxjs/operators"; -import {PaginatedResult} from "../../_models/pagination"; -import {SortEvent} from "../table/_directives/sortable-header.directive"; -import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {translate, TranslocoModule} from "@jsverse/transloco"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; -import {TranslocoLocaleModule} from "@jsverse/transloco-locale"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; -import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter"; -import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable"; -import {AsyncPipe} from "@angular/common"; -import {AccountService} from "../../_services/account.service"; -import {ToastrService} from "ngx-toastr"; -import {SelectionModel} from "../../typeahead/_models/selection-model"; - -export interface DataTablePage { - pageNumber: number, - size: number, - totalElements: number, - totalPages: number -} +import {PaginatedResult, Pagination} from "../../_models/pagination"; +import {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive"; +import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {TranslocoModule} from "@ngneat/transloco"; +import {DefaultValuePipe} from "../../pipe/default-value.pipe"; +import {TranslocoLocaleModule} from "@ngneat/transloco-locale"; +import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe"; @Component({ - selector: 'app-user-scrobble-history', - imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule, - DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe, FormsModule], - templateUrl: './user-scrobble-history.component.html', - styleUrls: ['./user-scrobble-history.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-user-scrobble-history', + standalone: true, + imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe], + templateUrl: './user-scrobble-history.component.html', + styleUrls: ['./user-scrobble-history.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class UserScrobbleHistoryComponent implements OnInit { - protected readonly SpecialVolumeNumber = SpecialVolumeNumber; - protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber; - protected readonly ColumnMode = ColumnMode; - protected readonly ScrobbleEventType = ScrobbleEventType; - - private readonly scrobblingService = inject(ScrobblingService); + private readonly scrobbleService = inject(ScrobblingService); private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); - private readonly toastr = inject(ToastrService); - protected readonly accountService = inject(AccountService); - tokenExpired = false; + pagination: Pagination | undefined; + events: Array = []; formGroup: FormGroup = new FormGroup({ 'filter': new FormControl('', []) }); - events: Array = []; - isLoading: boolean = true; - pageInfo: DataTablePage = { - pageNumber: 0, - size: 10, - totalElements: 0, - totalPages: 0 - } - private currentSort: SortEvent = { - column: 'lastModifiedUtc', - direction: 'desc' - }; - hasRunScrobbleGen: boolean = false; - selections: SelectionModel = new SelectionModel(); - selectAll: boolean = false; - isShiftDown: boolean = false; - lastSelectedIndex: number | null = null; - - @HostListener('document:keydown.shift', ['$event']) - handleKeypress(_: KeyboardEvent) { - this.isShiftDown = true; - } - - @HostListener('document:keyup.shift', ['$event']) - handleKeyUp(_: KeyboardEvent) { - this.isShiftDown = false; - } + get ScrobbleEventType() { return ScrobbleEventType; } ngOnInit() { - - this.pageInfo.pageNumber = 0; - this.cdRef.markForCheck(); - - this.scrobblingService.hasRunScrobbleGen().subscribe(res => { - this.hasRunScrobbleGen = res; - this.cdRef.markForCheck(); - }) - - this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => { - this.tokenExpired = hasExpired; - this.cdRef.markForCheck(); - }); + this.loadPage({column: 'createdUtc', direction: 'desc'}); this.formGroup.get('filter')?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe(query => { this.loadPage(); - }); - - this.loadPage(this.currentSort); + }) } - onPageChange(pageInfo: any) { - this.pageInfo.pageNumber = pageInfo.offset; - this.cdRef.markForCheck(); + onPageChange(pageNum: number) { + let prevPage = 0; + if (this.pagination) { + prevPage = this.pagination.currentPage; + this.pagination.currentPage = pageNum; + } + if (prevPage !== pageNum) { + this.loadPage(); + } - this.loadPage(this.currentSort); } - updateSort(data: any) { - this.currentSort = { - column: data.column.prop, - direction: data.newValue - }; + updateSort(sortEvent: SortEvent) { + this.loadPage(sortEvent); } loadPage(sortEvent?: SortEvent) { - const page = (this.pageInfo?.pageNumber || 0) + 1; - const pageSize = this.pageInfo?.size || 0; + if (sortEvent && this.pagination) { + this.pagination.currentPage = 1; + this.cdRef.markForCheck(); + } + const page = this.pagination?.currentPage || 0; + const pageSize = this.pagination?.itemsPerPage || 0; const isDescending = sortEvent?.direction === 'desc'; const field = this.mapSortColumnField(sortEvent?.column); const query = this.formGroup.get('filter')?.value; - this.isLoading = true; - this.cdRef.markForCheck(); - - this.scrobblingService.getScrobbleEvents({query, field, isDescending}, page, pageSize) + this.scrobbleService.getScrobbleEvents({query, field, isDescending}, page, pageSize) .pipe(take(1)) .subscribe((result: PaginatedResult) => { this.events = result.result; - this.selections = new SelectionModel(false, this.events); - - this.pageInfo.totalPages = result.pagination.totalPages - 1; // ngx-datatable is 0 based, Kavita is 1 based - this.pageInfo.size = result.pagination.itemsPerPage; - this.pageInfo.totalElements = result.pagination.totalItems; - this.isLoading = false; + this.pagination = result.pagination; this.cdRef.markForCheck(); }); } @@ -156,65 +88,9 @@ export class UserScrobbleHistoryComponent implements OnInit { case 'isProcessed': return ScrobbleEventSortField.IsProcessed; case 'lastModifiedUtc': return ScrobbleEventSortField.LastModified; case 'seriesName': return ScrobbleEventSortField.Series; - case 'scrobbleEventType': return ScrobbleEventSortField.ScrobbleEvent; } return ScrobbleEventSortField.None; } - generateScrobbleEvents() { - this.scrobblingService.triggerScrobbleEventGeneration().subscribe(_ => { - this.toastr.info(translate('toasts.scrobble-gen-init')) - }); - } - bulkDelete() { - if (!this.selections.hasAnySelected()) { - return; - } - - const eventIds = this.selections.selected().map(e => e.id); - - this.scrobblingService.bulkRemoveEvents(eventIds).subscribe({ - next: () => { - this.events = this.events.filter(e => !eventIds.includes(e.id)); - this.selectAll = false; - this.selections.clearSelected(); - this.pageInfo.totalElements -= eventIds.length; - this.cdRef.markForCheck(); - }, - error: err => { - console.error(err); - } - }); - } - - toggleAll() { - this.selectAll = !this.selectAll; - this.events.forEach(e => this.selections.toggle(e, this.selectAll)); - this.cdRef.markForCheck(); - } - - handleSelection(item: ScrobbleEvent, index: number) { - if (this.isShiftDown && this.lastSelectedIndex !== null) { - // Bulk select items between the last selected item and the current one - const start = Math.min(this.lastSelectedIndex, index); - const end = Math.max(this.lastSelectedIndex, index); - - for (let i = start; i <= end; i++) { - const event = this.events[i]; - if (!this.selections.isSelected(event, (e1, e2) => e1.id == e2.id)) { - this.selections.toggle(event, true); - } - } - } else { - this.selections.toggle(item); - } - - this.lastSelectedIndex = index; - - - const numberOfSelected = this.selections.selected().length; - this.selectAll = numberOfSelected === this.events.length; - this.cdRef.markForCheck(); - } } diff --git a/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.html b/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.html deleted file mode 100644 index f0056050b..000000000 --- a/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - diff --git a/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.ts b/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.ts deleted file mode 100644 index b8a1b41ea..000000000 --- a/UI/Web/src/app/admin/_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; -import {Library} from "../../../_models/library/library"; -import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; -import {TranslocoDirective} from "@jsverse/transloco"; -import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; - -@Component({ - selector: 'app-copy-settings-from-library-modal', - imports: [ - TranslocoDirective, - ReactiveFormsModule, - ], - templateUrl: './copy-settings-from-library-modal.component.html', - styleUrl: './copy-settings-from-library-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class CopySettingsFromLibraryModalComponent { - protected readonly modal = inject(NgbActiveModal); - - @Input() libraries: Array = []; - - libForm = new FormGroup({ - 'library': new FormControl(null), - }); - - save() { - this.modal.close(parseInt(this.libForm.get('library')?.value + '', 10)); - } -} diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html index d4ec401e3..80fc94896 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html @@ -8,7 +8,7 @@
- @@ -48,7 +48,7 @@ ... - + {{folder.name}} diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.scss b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.scss index a086f4ecf..bbe577134 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.scss +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.scss @@ -14,8 +14,12 @@ $breadcrumb-divider: quote(">"); border: 1px solid #ced4da; } +.table { + background-color: lightgrey; +} + .disabled { color: lightgrey !important; cursor: not-allowed !important; background-color: var(--error-color); -} +} \ No newline at end of file diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts index a1c97ee70..6d3d28d21 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts @@ -6,8 +6,7 @@ import { DirectoryDto } from 'src/app/_models/system/directory-dto'; import { LibraryService } from '../../../_services/library.service'; import { NgIf, NgFor, NgClass } from '@angular/common'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import {TranslocoDirective} from "@jsverse/transloco"; -import {WikiLink} from "../../../_models/wiki"; +import {TranslocoDirective} from "@ngneat/transloco"; export interface DirectoryPickerResult { @@ -15,11 +14,14 @@ export interface DirectoryPickerResult { folderPath: string; } + + @Component({ selector: 'app-directory-picker', templateUrl: './directory-picker.component.html', styleUrls: ['./directory-picker.component.scss'], - imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass, TranslocoDirective] + standalone: true, + imports: [ReactiveFormsModule, NgbTypeahead, FormsModule, NgbHighlight, NgIf, NgFor, NgClass, TranslocoDirective] }) export class DirectoryPickerComponent implements OnInit { @@ -27,7 +29,7 @@ export class DirectoryPickerComponent implements OnInit { /** * Url to give more information about selecting directories. Passing nothing will suppress. */ - @Input() helpUrl: string = WikiLink.Library; + @Input() helpUrl: string = 'https://wiki.kavitareader.com/en/guides/first-time-setup#adding-a-library-to-kavita'; currentRoot = ''; folders: DirectoryDto[] = []; @@ -35,7 +37,7 @@ export class DirectoryPickerComponent implements OnInit { path: string = ''; - @ViewChild('instance', {static: false}) instance!: NgbTypeahead; + @ViewChild('instance', {static: true}) instance!: NgbTypeahead; focus$ = new Subject(); click$ = new Subject(); searching: boolean = false; @@ -127,6 +129,13 @@ export class DirectoryPickerComponent implements OnInit { }); } + shareFolder(fullPath: string, event: any) { + event.preventDefault(); + event.stopPropagation(); + + this.modal.close({success: true, folderPath: fullPath}); + } + share() { this.modal.close({success: true, folderPath: this.path}); } @@ -135,6 +144,20 @@ export class DirectoryPickerComponent implements OnInit { this.modal.close({success: false, folderPath: undefined}); } + getStem(path: string): string { + + const lastPath = this.routeStack.peek(); + if (lastPath && lastPath != path) { + let replaced = path.replace(lastPath, ''); + if (replaced.startsWith('/') || replaced.startsWith('\\')) { + replaced = replaced.substring(1, replaced.length); + } + return replaced; + } + + return path; + } + navigateTo(index: number) { while(this.routeStack.items.length - 1 > index) { this.routeStack.pop(); diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html index 1cb11d21a..a205bd4f9 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.html @@ -14,19 +14,16 @@
    - @for (library of allLibraries; track library.name; let i = $index) { -
  • -
    - - -
    -
  • - } @empty { -
  • - {{t('no-data')}} -
  • - } +
  • +
    + + +
    +
  • +
  • + {{t('no-data')}} +
diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts index 3eb8c080c..ff23e16ee 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts @@ -1,37 +1,36 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import {Library} from 'src/app/_models/library/library'; +import {Library} from 'src/app/_models/library'; import {Member} from 'src/app/_models/auth/member'; import {LibraryService} from 'src/app/_services/library.service'; +import {SelectionModel} from 'src/app/typeahead/_components/typeahead.component'; +import {NgFor, NgIf} from '@angular/common'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {TranslocoDirective} from "@jsverse/transloco"; -import {SelectionModel} from "../../../typeahead/_models/selection-model"; +import {TranslocoDirective} from "@ngneat/transloco"; @Component({ selector: 'app-library-access-modal', templateUrl: './library-access-modal.component.html', styleUrls: ['./library-access-modal.component.scss'], standalone: true, - imports: [ReactiveFormsModule, FormsModule, TranslocoDirective], + imports: [ReactiveFormsModule, FormsModule, NgFor, NgIf, TranslocoDirective], changeDetection: ChangeDetectionStrategy.OnPush }) export class LibraryAccessModalComponent implements OnInit { - protected readonly modal = inject(NgbActiveModal); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly libraryService = inject(LibraryService); - @Input() member: Member | undefined; - allLibraries: Library[] = []; selectedLibraries: Array<{selected: boolean, data: Library}> = []; selections!: SelectionModel; selectAll: boolean = false; + cdRef = inject(ChangeDetectorRef); + get hasSomeSelected() { return this.selections != null && this.selections.hasSomeSelected(); } + constructor(public modal: NgbActiveModal, private libraryService: LibraryService) { } ngOnInit(): void { this.libraryService.getLibraries().subscribe(libs => { diff --git a/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.html b/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.html index 258503e94..716f95e78 100644 --- a/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.html +++ b/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.html @@ -7,12 +7,9 @@