Merge branch 'develop'
This commit is contained in:
commit
325f3804ab
296 changed files with 12589 additions and 22086 deletions
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**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]
|
||||||
|
- 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.
|
||||||
88
.github/workflows/nightly-docker.yml
vendored
88
.github/workflows/nightly-docker.yml
vendored
|
|
@ -1,88 +0,0 @@
|
||||||
name: Build Nightly Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'develop'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
name: Building Nightly Docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Check Out Repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: NodeJS to Compile WebUI
|
|
||||||
uses: actions/setup-node@v2.1.5
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
- run: |
|
|
||||||
cd UI/Web || exit
|
|
||||||
echo 'Installing web dependencies'
|
|
||||||
npm install
|
|
||||||
|
|
||||||
echo 'Building UI'
|
|
||||||
npm run prod
|
|
||||||
|
|
||||||
echo 'Copying back to Kavita wwwroot'
|
|
||||||
rsync -a dist/ ../../API/wwwroot/
|
|
||||||
|
|
||||||
cd ../ || exit
|
|
||||||
|
|
||||||
- name: Get csproj Version
|
|
||||||
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
|
||||||
id: get-version
|
|
||||||
with:
|
|
||||||
proj-path: Kavita.Common/Kavita.Common.csproj
|
|
||||||
|
|
||||||
- name: Echo csproj version
|
|
||||||
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
|
||||||
|
|
||||||
- name: Compile dotnet app
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '5.0.x'
|
|
||||||
- run: ./monorepo-build.sh
|
|
||||||
|
|
||||||
- name: Trigger Sentry workflow
|
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
|
||||||
with:
|
|
||||||
workflow: Sentry Map Release
|
|
||||||
token: ${{ secrets.REPO_GHA_PAT }}
|
|
||||||
inputs: '{ "version": "${{steps.get-version.outputs.assembly-version}}" }'
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: kizaing/kavita:nightly
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
|
|
||||||
- name: Notify Discord
|
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
|
||||||
with:
|
|
||||||
severity: info
|
|
||||||
description:
|
|
||||||
details: 'https://hub.docker.com/r/kizaing/kavita/tags?page=1&ordering=last_updated'
|
|
||||||
text: A new nightly build has been released for docker.
|
|
||||||
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
|
||||||
275
.github/workflows/sonar-scan.yml
vendored
275
.github/workflows/sonar-scan.yml
vendored
|
|
@ -2,14 +2,42 @@ name: .NET Build Test and Sonar Scan
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main, develop ]
|
branches: '**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
types: [opened, synchronize, reopened]
|
types: [synchronize]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Scan
|
name: Build .Net
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 5.0.100
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.11
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: csproj
|
||||||
|
path: Kavita.Common/Kavita.Common.csproj
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Install Sonar & Test
|
||||||
|
needs: build
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
|
|
@ -52,7 +80,7 @@ jobs:
|
||||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||||
|
|
||||||
- name: Build and analyze
|
- name: Sonar Scan
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|
@ -64,3 +92,242 @@ jobs:
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --verbosity normal
|
run: dotnet test --no-restore --verbosity normal
|
||||||
|
|
||||||
|
version:
|
||||||
|
name: Bump version on Develop push
|
||||||
|
needs: [ build, test ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 5.0.100
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Bump versions
|
||||||
|
uses: SiqiLu/dotnet-bump-version@master
|
||||||
|
with:
|
||||||
|
version_files: Kavita.Common/Kavita.Common.csproj
|
||||||
|
github_token: ${{ secrets.REPO_GHA_PAT }}
|
||||||
|
|
||||||
|
develop:
|
||||||
|
name: Build Nightly Docker if Develop push
|
||||||
|
needs: [ build, test, version ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
|
steps:
|
||||||
|
- name: Find Current Pull Request
|
||||||
|
uses: jwalton/gh-find-current-pr@v1.0.2
|
||||||
|
id: findPr
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Parse PR body
|
||||||
|
id: parse-body
|
||||||
|
run: |
|
||||||
|
body="${{ steps.findPr.outputs.body }}"
|
||||||
|
body=${body//\'/}
|
||||||
|
body=${body//'%'/'%25'}
|
||||||
|
body=${body//$'\n'/'%0A'}
|
||||||
|
body=${body//$'\r'/'%0D'}
|
||||||
|
echo $body
|
||||||
|
echo "::set-output name=BODY::$body"
|
||||||
|
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: develop
|
||||||
|
|
||||||
|
- name: NodeJS to Compile WebUI
|
||||||
|
uses: actions/setup-node@v2.1.5
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
- run: |
|
||||||
|
cd UI/Web || exit
|
||||||
|
echo 'Installing web dependencies'
|
||||||
|
npm install
|
||||||
|
|
||||||
|
echo 'Building UI'
|
||||||
|
npm run prod
|
||||||
|
|
||||||
|
echo 'Copying back to Kavita wwwroot'
|
||||||
|
rsync -a dist/ ../../API/wwwroot/
|
||||||
|
|
||||||
|
cd ../ || exit
|
||||||
|
|
||||||
|
- name: Get csproj Version
|
||||||
|
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
||||||
|
id: get-version
|
||||||
|
with:
|
||||||
|
proj-path: Kavita.Common/Kavita.Common.csproj
|
||||||
|
|
||||||
|
- name: Echo csproj version
|
||||||
|
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
||||||
|
|
||||||
|
- name: Compile dotnet app
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '5.0.x'
|
||||||
|
- run: ./monorepo-build.sh
|
||||||
|
|
||||||
|
- name: Trigger Sentry workflow
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: Sentry Map Release
|
||||||
|
token: ${{ secrets.REPO_GHA_PAT }}
|
||||||
|
inputs: '{ "version": "${{steps.get-version.outputs.assembly-version}}" }'
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: kizaing/kavita:nightly
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.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.parse-body.outputs.BODY }}'
|
||||||
|
text: A new nightly build has been released for docker.
|
||||||
|
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
||||||
|
|
||||||
|
stable:
|
||||||
|
name: Build Stable Docker if Main push
|
||||||
|
needs: [ build, test ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Find Current Pull Request
|
||||||
|
uses: jwalton/gh-find-current-pr@v1.0.2
|
||||||
|
id: findPr
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Parse PR body
|
||||||
|
id: parse-body
|
||||||
|
run: |
|
||||||
|
body="${{ steps.findPr.outputs.body }}"
|
||||||
|
body=${body//\'/}
|
||||||
|
body=${body//'%'/'%25'}
|
||||||
|
body=${body//$'\n'/'%0A'}
|
||||||
|
body=${body//$'\r'/'%0D'}
|
||||||
|
echo $body
|
||||||
|
echo "::set-output name=BODY::$body"
|
||||||
|
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: NodeJS to Compile WebUI
|
||||||
|
uses: actions/setup-node@v2.1.5
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
- run: |
|
||||||
|
|
||||||
|
cd UI/Web || exit
|
||||||
|
echo 'Installing web dependencies'
|
||||||
|
npm install
|
||||||
|
|
||||||
|
echo 'Building UI'
|
||||||
|
npm run prod
|
||||||
|
|
||||||
|
echo 'Copying back to Kavita wwwroot'
|
||||||
|
rsync -a dist/ ../../API/wwwroot/
|
||||||
|
|
||||||
|
cd ../ || exit
|
||||||
|
|
||||||
|
- name: Get csproj Version
|
||||||
|
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
||||||
|
id: get-version
|
||||||
|
with:
|
||||||
|
proj-path: Kavita.Common/Kavita.Common.csproj
|
||||||
|
|
||||||
|
- name: Echo csproj version
|
||||||
|
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
||||||
|
|
||||||
|
- name: Parse Version
|
||||||
|
run: |
|
||||||
|
version='${{steps.get-version.outputs.assembly-version}}'
|
||||||
|
newVersion=${version%.*}
|
||||||
|
echo $newVersion
|
||||||
|
echo "::set-output name=VERSION::$newVersion"
|
||||||
|
id: parse-version
|
||||||
|
|
||||||
|
- name: Compile dotnet app
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '5.0.x'
|
||||||
|
- run: ./monorepo-build.sh
|
||||||
|
|
||||||
|
- name: Trigger Sentry workflow
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: Sentry Map Release
|
||||||
|
token: ${{ secrets.REPO_GHA_PAT }}
|
||||||
|
inputs: '{ "version": "${{steps.get-version.outputs.assembly-version}}" }'
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: kizaing/kavita:latest, kizaing/kavita:${{ steps.parse-version.outputs.VERSION }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.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.parse-body.outputs.BODY }}'
|
||||||
|
text: A new stable build has been released.
|
||||||
|
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
||||||
|
|
|
||||||
88
.github/workflows/stable-docker.yml
vendored
88
.github/workflows/stable-docker.yml
vendored
|
|
@ -1,88 +0,0 @@
|
||||||
name: Build Stable Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Check Out Repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: NodeJS to Compile WebUI
|
|
||||||
uses: actions/setup-node@v2.1.5
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
- run: |
|
|
||||||
|
|
||||||
cd UI/Web || exit
|
|
||||||
echo 'Installing web dependencies'
|
|
||||||
npm install
|
|
||||||
|
|
||||||
echo 'Building UI'
|
|
||||||
npm run prod
|
|
||||||
|
|
||||||
echo 'Copying back to Kavita wwwroot'
|
|
||||||
rsync -a dist/ ../../API/wwwroot/
|
|
||||||
|
|
||||||
cd ../ || exit
|
|
||||||
|
|
||||||
- name: Get csproj Version
|
|
||||||
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
|
||||||
id: get-version
|
|
||||||
with:
|
|
||||||
proj-path: Kavita.Common/Kavita.Common.csproj
|
|
||||||
|
|
||||||
- name: Echo csproj version
|
|
||||||
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
|
||||||
|
|
||||||
- name: Compile dotnet app
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '5.0.x'
|
|
||||||
- run: ./monorepo-build.sh
|
|
||||||
|
|
||||||
- name: Trigger Sentry workflow
|
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
|
||||||
with:
|
|
||||||
workflow: Sentry Map Release
|
|
||||||
token: ${{ secrets.REPO_GHA_PAT }}
|
|
||||||
inputs: '{ "version": "${{steps.get-version.outputs.assembly-version}}" }'
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: kizaing/kavita:latest
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
|
|
||||||
- name: Notify Discord
|
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
|
||||||
with:
|
|
||||||
severity: info
|
|
||||||
description:
|
|
||||||
details: 'https://hub.docker.com/r/kizaing/kavita/tags?page=1&ordering=last_updated'
|
|
||||||
text: A new stable build has been released for docker.
|
|
||||||
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -499,3 +499,4 @@ _temp/
|
||||||
_output/
|
_output/
|
||||||
API/stats/
|
API/stats/
|
||||||
UI/Web/dist/
|
UI/Web/dist/
|
||||||
|
/API.Tests/Extensions/Test Data/modified on run.txt
|
||||||
|
|
|
||||||
18
API.Benchmark/API.Benchmark.csproj
Normal file
18
API.Benchmark/API.Benchmark.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\API\API.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||||
|
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
40
API.Benchmark/ParseScannedFilesBenchmarks.cs
Normal file
40
API.Benchmark/ParseScannedFilesBenchmarks.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Order;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace API.Benchmark
|
||||||
|
{
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
[RankColumn]
|
||||||
|
[SimpleJob(launchCount: 1, warmupCount: 3, targetCount: 5, invocationCount: 100, id: "Test"), ShortRunJob]
|
||||||
|
public class ParseScannedFilesBenchmarks
|
||||||
|
{
|
||||||
|
private readonly ParseScannedFiles _parseScannedFiles;
|
||||||
|
private readonly ILogger<ParseScannedFiles> _logger = Substitute.For<ILogger<ParseScannedFiles>>();
|
||||||
|
private readonly ILogger<BookService> _bookLogger = Substitute.For<ILogger<BookService>>();
|
||||||
|
|
||||||
|
public ParseScannedFilesBenchmarks()
|
||||||
|
{
|
||||||
|
IBookService bookService = new BookService(_bookLogger);
|
||||||
|
_parseScannedFiles = new ParseScannedFiles(bookService, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void Test()
|
||||||
|
{
|
||||||
|
var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
|
||||||
|
"../../../Services/Test Data/ScannerService/Manga");
|
||||||
|
var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
|
||||||
|
out var totalFiles, out var scanElapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
API.Benchmark/Program.cs
Normal file
18
API.Benchmark/Program.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
namespace API.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// To build this, cd into API.Benchmark directory and run
|
||||||
|
/// dotnet build -c Release
|
||||||
|
/// then copy the outputted dll
|
||||||
|
/// dotnet copied_string\API.Benchmark.dll
|
||||||
|
/// </summary>
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,15 +7,15 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.0.3">
|
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
namespace API.Tests.Extensions
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using API.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions
|
||||||
{
|
{
|
||||||
public class FileInfoExtensionsTests
|
public class FileInfoExtensionsTests
|
||||||
{
|
{
|
||||||
// [Fact]
|
private static readonly string TestDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Extensions/Test Data/");
|
||||||
// public void DoesLastWriteMatchTest()
|
|
||||||
// {
|
[Fact]
|
||||||
// var fi = Substitute.For<FileInfo>();
|
public void HasFileBeenModifiedSince_ShouldBeFalse()
|
||||||
// fi.LastWriteTime = DateTime.Now;
|
{
|
||||||
//
|
var filepath = Path.Join(TestDirectory, "not modified.txt");
|
||||||
// var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(1));
|
var date = new FileInfo(filepath).LastWriteTime;
|
||||||
// Assert.False(fi.DoesLastWriteMatch(deltaTime));
|
Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
||||||
// }
|
File.ReadAllText(filepath);
|
||||||
//
|
Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
||||||
// [Fact]
|
}
|
||||||
// public void IsLastWriteLessThanTest()
|
|
||||||
// {
|
[Fact]
|
||||||
//
|
public void HasFileBeenModifiedSince_ShouldBeTrue()
|
||||||
// }
|
{
|
||||||
|
var filepath = Path.Join(TestDirectory, "modified on run.txt");
|
||||||
|
var date = new FileInfo(filepath).LastWriteTime;
|
||||||
|
Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
||||||
|
File.AppendAllLines(filepath, new[] { DateTime.Now.ToString(CultureInfo.InvariantCulture) });
|
||||||
|
Assert.True(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
API.Tests/Extensions/Test Data/modified on run.txt
Normal file
3
API.Tests/Extensions/Test Data/modified on run.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
This file should be modified by the unit test08/20/2021 10:26:03
|
||||||
|
08/20/2021 10:26:29
|
||||||
|
08/22/2021 12:39:58
|
||||||
1
API.Tests/Extensions/Test Data/not modified.txt
Normal file
1
API.Tests/Extensions/Test Data/not modified.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Hello, this file should not be modified
|
||||||
|
|
@ -22,11 +22,12 @@ namespace API.Tests.Parser
|
||||||
[InlineData("Invincible Vol 01 Family matters (2005) (Digital).cbr", "Invincible")]
|
[InlineData("Invincible Vol 01 Family matters (2005) (Digital).cbr", "Invincible")]
|
||||||
[InlineData("Amazing Man Comics chapter 25", "Amazing Man Comics")]
|
[InlineData("Amazing Man Comics chapter 25", "Amazing Man Comics")]
|
||||||
[InlineData("Amazing Man Comics issue #25", "Amazing Man Comics")]
|
[InlineData("Amazing Man Comics issue #25", "Amazing Man Comics")]
|
||||||
|
[InlineData("Teen Titans v1 038 (1972) (c2c).cbr", "Teen Titans")]
|
||||||
public void ParseComicSeriesTest(string filename, string expected)
|
public void ParseComicSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
|
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
|
||||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")]
|
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")]
|
||||||
|
|
@ -47,7 +48,7 @@ namespace API.Tests.Parser
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "0")]
|
[InlineData("01 Spider-Man & Wolverine 01.cbr", "0")]
|
||||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
||||||
|
|
@ -70,4 +71,4 @@ namespace API.Tests.Parser
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -26,6 +26,7 @@ namespace API.Tests.Parser
|
||||||
[InlineData("Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", "1")]
|
[InlineData("Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", "1")]
|
||||||
[InlineData("v001", "1")]
|
[InlineData("v001", "1")]
|
||||||
[InlineData("Vol 1", "1")]
|
[InlineData("Vol 1", "1")]
|
||||||
|
[InlineData("vol_356-1", "356")] // Mangapy syntax
|
||||||
[InlineData("No Volume", "0")]
|
[InlineData("No Volume", "0")]
|
||||||
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
|
||||||
|
|
@ -64,6 +65,7 @@ namespace API.Tests.Parser
|
||||||
[InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")]
|
[InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")]
|
||||||
[InlineData("Noblesse - Episode 406 (52 Pages).7z", "0")]
|
[InlineData("Noblesse - Episode 406 (52 Pages).7z", "0")]
|
||||||
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
|
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
|
||||||
|
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
|
||||||
|
|
@ -154,6 +156,7 @@ namespace API.Tests.Parser
|
||||||
[InlineData("Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz", "Please Go Home, Akutsu-San!")]
|
[InlineData("Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz", "Please Go Home, Akutsu-San!")]
|
||||||
[InlineData("Killing Bites - Vol 11 Chapter 050 Save Me, Nunupi!.cbz", "Killing Bites")]
|
[InlineData("Killing Bites - Vol 11 Chapter 050 Save Me, Nunupi!.cbz", "Killing Bites")]
|
||||||
[InlineData("Mad Chimera World - Volume 005 - Chapter 026.cbz", "Mad Chimera World")]
|
[InlineData("Mad Chimera World - Volume 005 - Chapter 026.cbz", "Mad Chimera World")]
|
||||||
|
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "Hentai Ouji to Warawanai Neko.")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
|
||||||
|
|
@ -222,6 +225,7 @@ namespace API.Tests.Parser
|
||||||
[InlineData("Boku No Kokoro No Yabai Yatsu - Chapter 054 I Prayed At The Shrine (V0).cbz", "54")]
|
[InlineData("Boku No Kokoro No Yabai Yatsu - Chapter 054 I Prayed At The Shrine (V0).cbz", "54")]
|
||||||
[InlineData("Ijousha No Ai - Vol.01 Chapter 029 8 Years Ago", "29")]
|
[InlineData("Ijousha No Ai - Vol.01 Chapter 029 8 Years Ago", "29")]
|
||||||
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", "9")]
|
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", "9")]
|
||||||
|
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "34.5")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
|
||||||
|
|
@ -275,6 +279,7 @@ namespace API.Tests.Parser
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile));
|
Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
private static ParserInfo CreateParserInfo(string series, string chapter, string volume, bool isSpecial = false)
|
private static ParserInfo CreateParserInfo(string series, string chapter, string volume, bool isSpecial = false)
|
||||||
{
|
{
|
||||||
return new ParserInfo()
|
return new ParserInfo()
|
||||||
|
|
@ -285,6 +290,7 @@ namespace API.Tests.Parser
|
||||||
Series = series,
|
Series = series,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
|
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
|
||||||
|
|
@ -307,14 +313,14 @@ namespace API.Tests.Parser
|
||||||
const string rootPath = @"E:/Manga/";
|
const string rootPath = @"E:/Manga/";
|
||||||
var expected = new Dictionary<string, ParserInfo>();
|
var expected = new Dictionary<string, ParserInfo>();
|
||||||
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
|
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
|
||||||
expected.Add(filepath, new ParserInfo
|
expected.Add(filepath, new ParserInfo
|
||||||
{
|
{
|
||||||
Series = "Mujaki no Rakuen", Volumes = "12",
|
Series = "Mujaki no Rakuen", Volumes = "12",
|
||||||
Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive,
|
Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive,
|
||||||
FullFilePath = filepath
|
FullFilePath = filepath
|
||||||
});
|
});
|
||||||
|
|
||||||
filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz";
|
filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz";
|
||||||
expected.Add(filepath, new ParserInfo
|
expected.Add(filepath, new ParserInfo
|
||||||
{
|
{
|
||||||
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
|
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
|
||||||
|
|
@ -402,6 +408,10 @@ namespace API.Tests.Parser
|
||||||
FullFilePath = filepath, IsSpecial = false
|
FullFilePath = filepath, IsSpecial = false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If an image is cover exclusively, ignore it
|
||||||
|
filepath = @"E:\Manga\Seraph of the End\cover.png";
|
||||||
|
expected.Add(filepath, null);
|
||||||
|
|
||||||
|
|
||||||
foreach (var file in expected.Keys)
|
foreach (var file in expected.Keys)
|
||||||
{
|
{
|
||||||
|
|
@ -414,20 +424,20 @@ namespace API.Tests.Parser
|
||||||
}
|
}
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
_testOutputHelper.WriteLine($"Validating {file}");
|
_testOutputHelper.WriteLine($"Validating {file}");
|
||||||
_testOutputHelper.WriteLine("Format");
|
|
||||||
Assert.Equal(expectedInfo.Format, actual.Format);
|
Assert.Equal(expectedInfo.Format, actual.Format);
|
||||||
_testOutputHelper.WriteLine("Series");
|
_testOutputHelper.WriteLine("Format ✓");
|
||||||
Assert.Equal(expectedInfo.Series, actual.Series);
|
Assert.Equal(expectedInfo.Series, actual.Series);
|
||||||
_testOutputHelper.WriteLine("Chapters");
|
_testOutputHelper.WriteLine("Series ✓");
|
||||||
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
|
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
|
||||||
_testOutputHelper.WriteLine("Volumes");
|
_testOutputHelper.WriteLine("Chapters ✓");
|
||||||
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
|
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
|
||||||
_testOutputHelper.WriteLine("Edition");
|
_testOutputHelper.WriteLine("Volumes ✓");
|
||||||
Assert.Equal(expectedInfo.Edition, actual.Edition);
|
Assert.Equal(expectedInfo.Edition, actual.Edition);
|
||||||
_testOutputHelper.WriteLine("Filename");
|
_testOutputHelper.WriteLine("Edition ✓");
|
||||||
Assert.Equal(expectedInfo.Filename, actual.Filename);
|
Assert.Equal(expectedInfo.Filename, actual.Filename);
|
||||||
_testOutputHelper.WriteLine("FullFilePath");
|
_testOutputHelper.WriteLine("Filename ✓");
|
||||||
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
|
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
|
||||||
|
_testOutputHelper.WriteLine("FullFilePath ✓");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
API.Tests/Services/MetadataServiceTests.cs
Normal file
112
API.Tests/Services/MetadataServiceTests.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Services
|
||||||
|
{
|
||||||
|
public class MetadataServiceTests
|
||||||
|
{
|
||||||
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||||
|
private readonly MetadataService _metadataService;
|
||||||
|
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||||
|
private readonly IImageService _imageService = Substitute.For<IImageService>();
|
||||||
|
private readonly IBookService _bookService = Substitute.For<IBookService>();
|
||||||
|
private readonly IArchiveService _archiveService = Substitute.For<IArchiveService>();
|
||||||
|
private readonly ILogger<MetadataService> _logger = Substitute.For<ILogger<MetadataService>>();
|
||||||
|
|
||||||
|
public MetadataServiceTests()
|
||||||
|
{
|
||||||
|
_metadataService = new MetadataService(_unitOfWork, _logger, _archiveService, _bookService, _imageService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnFirstRun()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = DateTime.Now
|
||||||
|
}, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnFirstRunSeries()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null,null, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_FileModified()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime.Subtract(TimeSpan.FromDays(1))
|
||||||
|
}, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_CoverImageLocked()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.False(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||||
|
}, false, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_ForceUpdate()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||||
|
}, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_NoFileChangeButNoCoverImage()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||||
|
}, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_FileChangeButNoCoverImage()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime + TimeSpan.FromDays(1)
|
||||||
|
}, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldUpdateCoverImage_OnSecondRun_CoverImageSet()
|
||||||
|
{
|
||||||
|
// Represents first run
|
||||||
|
Assert.False(MetadataService.ShouldUpdateCoverImage(new byte[] {1}, new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||||
|
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||||
|
}, false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,6 @@ namespace API.Tests.Services
|
||||||
private readonly IBookService _bookService = Substitute.For<IBookService>();
|
private readonly IBookService _bookService = Substitute.For<IBookService>();
|
||||||
private readonly IImageService _imageService = Substitute.For<IImageService>();
|
private readonly IImageService _imageService = Substitute.For<IImageService>();
|
||||||
private readonly ILogger<MetadataService> _metadataLogger = Substitute.For<ILogger<MetadataService>>();
|
private readonly ILogger<MetadataService> _metadataLogger = Substitute.For<ILogger<MetadataService>>();
|
||||||
private readonly IDirectoryService _directoryService = Substitute.For<IDirectoryService>();
|
|
||||||
private readonly ICacheService _cacheService = Substitute.For<ICacheService>();
|
private readonly ICacheService _cacheService = Substitute.For<ICacheService>();
|
||||||
|
|
||||||
private readonly DbConnection _connection;
|
private readonly DbConnection _connection;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@
|
||||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<DocumentationFile>bin\Debug\API.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Set the Product and Version info for our own projects -->
|
<!-- Set the Product and Version info for our own projects -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Product>Kavita</Product>
|
<Product>Kavita</Product>
|
||||||
|
|
@ -33,33 +37,37 @@
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||||
<PackageReference Include="Docnet.Core" Version="2.3.1" />
|
<PackageReference Include="Docnet.Core" Version="2.3.1" />
|
||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.20" />
|
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.20" />
|
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
||||||
|
<PackageReference Include="Hangfire" Version="1.7.24" />
|
||||||
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.24" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.32" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.35" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.0.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||||
<PackageReference Include="NetVips" Version="2.0.1" />
|
<PackageReference Include="NetVips" Version="2.0.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.2" />
|
<PackageReference Include="Sentry.AspNetCore" Version="3.8.3" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.26.0.34506">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.0" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
@ -73,22 +81,45 @@
|
||||||
<None Remove="Hangfire-log.db" />
|
<None Remove="Hangfire-log.db" />
|
||||||
<None Remove="obj\**" />
|
<None Remove="obj\**" />
|
||||||
<None Remove="wwwroot\**" />
|
<None Remove="wwwroot\**" />
|
||||||
|
<None Remove="cache\**" />
|
||||||
|
<None Remove="backups\**" />
|
||||||
|
<None Remove="logs\**" />
|
||||||
|
<None Remove="temp\**" />
|
||||||
|
<None Remove="kavita.log" />
|
||||||
|
<None Remove="kavita.db" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||||
<Compile Remove="obj\**" />
|
<Compile Remove="obj\**" />
|
||||||
<Compile Remove="wwwroot\**" />
|
<Compile Remove="wwwroot\**" />
|
||||||
|
<Compile Remove="cache\**" />
|
||||||
|
<Compile Remove="backups\**" />
|
||||||
|
<Compile Remove="logs\**" />
|
||||||
|
<Compile Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Remove="obj\**" />
|
<EmbeddedResource Remove="obj\**" />
|
||||||
<EmbeddedResource Remove="wwwroot\**" />
|
<EmbeddedResource Remove="wwwroot\**" />
|
||||||
|
<EmbeddedResource Remove="cache\**" />
|
||||||
|
<EmbeddedResource Remove="backups\**" />
|
||||||
|
<EmbeddedResource Remove="logs\**" />
|
||||||
|
<EmbeddedResource Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Remove="obj\**" />
|
<Content Remove="obj\**" />
|
||||||
<Content Remove="wwwroot\**" />
|
<Content Remove="wwwroot\**" />
|
||||||
|
<Content Remove="cache\**" />
|
||||||
|
<Content Remove="backups\**" />
|
||||||
|
<Content Remove="logs\**" />
|
||||||
|
<Content Remove="temp\**" />
|
||||||
|
<Content Remove="stats\**" />
|
||||||
|
<Content Condition=" '$(Configuration)' == 'Release' " Remove="appsettings.Development.json" />
|
||||||
|
<Content Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -215,4 +246,8 @@
|
||||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js.map" />
|
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js.map" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System.Drawing.Common" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,17 @@
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ArchiveLibrary
|
public enum ArchiveLibrary
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive cannot be opened
|
||||||
|
/// </summary>
|
||||||
NotSupported = 0,
|
NotSupported = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive can be opened by SharpCompress
|
||||||
|
/// </summary>
|
||||||
SharpCompress = 1,
|
SharpCompress = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive can be opened by default .NET
|
||||||
|
/// </summary>
|
||||||
Default = 2
|
Default = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace API.Archive
|
|
||||||
{
|
|
||||||
public class CoverAndPages
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
namespace API.Constants
|
namespace API.Constants
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Role-based Security
|
||||||
|
/// </summary>
|
||||||
public static class PolicyConstants
|
public static class PolicyConstants
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin User. Has all privileges
|
||||||
|
/// </summary>
|
||||||
public const string AdminRole = "Admin";
|
public const string AdminRole = "Admin";
|
||||||
|
/// <summary>
|
||||||
|
/// Non-Admin User. Must be granted privileges by an Admin.
|
||||||
|
/// </summary>
|
||||||
public const string PlebRole = "Pleb";
|
public const string PlebRole = "Pleb";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to give a user ability to download files from the server
|
/// Used to give a user ability to download files from the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string DownloadRole = "Download";
|
public const string DownloadRole = "Download";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -18,6 +19,9 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All Account matters
|
||||||
|
/// </summary>
|
||||||
public class AccountController : BaseApiController
|
public class AccountController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
|
@ -27,9 +31,10 @@ namespace API.Controllers
|
||||||
private readonly ILogger<AccountController> _logger;
|
private readonly ILogger<AccountController> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public AccountController(UserManager<AppUser> userManager,
|
public AccountController(UserManager<AppUser> userManager,
|
||||||
SignInManager<AppUser> signInManager,
|
SignInManager<AppUser> signInManager,
|
||||||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||||
ILogger<AccountController> logger,
|
ILogger<AccountController> logger,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
|
|
@ -40,7 +45,12 @@ namespace API.Controllers
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update a user's password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resetPasswordDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("reset-password")]
|
[HttpPost("reset-password")]
|
||||||
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||||
{
|
{
|
||||||
|
|
@ -49,7 +59,7 @@ namespace API.Controllers
|
||||||
|
|
||||||
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
||||||
return Unauthorized("You are not permitted to this operation.");
|
return Unauthorized("You are not permitted to this operation.");
|
||||||
|
|
||||||
// Validate Password
|
// Validate Password
|
||||||
foreach (var validator in _userManager.PasswordValidators)
|
foreach (var validator in _userManager.PasswordValidators)
|
||||||
{
|
{
|
||||||
|
|
@ -60,26 +70,31 @@ namespace API.Controllers
|
||||||
validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _userManager.RemovePasswordAsync(user);
|
var result = await _userManager.RemovePasswordAsync(user);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not update password");
|
_logger.LogError("Could not update password");
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not update password");
|
_logger.LogError("Could not update password");
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new user on the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registerDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
||||||
{
|
{
|
||||||
|
|
@ -92,6 +107,7 @@ namespace API.Controllers
|
||||||
|
|
||||||
var user = _mapper.Map<AppUser>(registerDto);
|
var user = _mapper.Map<AppUser>(registerDto);
|
||||||
user.UserPreferences ??= new AppUserPreferences();
|
user.UserPreferences ??= new AppUserPreferences();
|
||||||
|
user.ApiKey = HashUtil.ApiKey();
|
||||||
|
|
||||||
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
||||||
|
|
||||||
|
|
@ -122,6 +138,7 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
Username = user.UserName,
|
Username = user.UserName,
|
||||||
Token = await _tokenService.CreateToken(user),
|
Token = await _tokenService.CreateToken(user),
|
||||||
|
ApiKey = user.ApiKey,
|
||||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +151,11 @@ namespace API.Controllers
|
||||||
return BadRequest("Something went wrong when registering user");
|
return BadRequest("Something went wrong when registering user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a login. Will send JWT Token of the logged in user back.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loginDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||||
{
|
{
|
||||||
|
|
@ -147,24 +169,29 @@ namespace API.Controllers
|
||||||
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
||||||
|
|
||||||
if (!result.Succeeded) return Unauthorized("Your credentials are not correct.");
|
if (!result.Succeeded) return Unauthorized("Your credentials are not correct.");
|
||||||
|
|
||||||
// Update LastActive on account
|
// Update LastActive on account
|
||||||
user.LastActive = DateTime.Now;
|
user.LastActive = DateTime.Now;
|
||||||
user.UserPreferences ??= new AppUserPreferences();
|
user.UserPreferences ??= new AppUserPreferences();
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
||||||
|
|
||||||
return new UserDto
|
return new UserDto
|
||||||
{
|
{
|
||||||
Username = user.UserName,
|
Username = user.UserName,
|
||||||
Token = await _tokenService.CreateToken(user),
|
Token = await _tokenService.CreateToken(user),
|
||||||
|
ApiKey = user.ApiKey,
|
||||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get All Roles back. See <see cref="PolicyConstants"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("roles")]
|
[HttpGet("roles")]
|
||||||
public ActionResult<IList<string>> GetRoles()
|
public ActionResult<IList<string>> GetRoles()
|
||||||
{
|
{
|
||||||
|
|
@ -175,6 +202,11 @@ namespace API.Controllers
|
||||||
f => (string) f.GetValue(null)).Values.ToList();
|
f => (string) f.GetValue(null)).Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the given roles to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateRbsDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("update-rbs")]
|
[HttpPost("update-rbs")]
|
||||||
public async Task<ActionResult> UpdateRoles(UpdateRbsDto updateRbsDto)
|
public async Task<ActionResult> UpdateRoles(UpdateRbsDto updateRbsDto)
|
||||||
{
|
{
|
||||||
|
|
@ -190,7 +222,7 @@ namespace API.Controllers
|
||||||
var existingRoles = (await _userManager.GetRolesAsync(user))
|
var existingRoles = (await _userManager.GetRolesAsync(user))
|
||||||
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
|
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Find what needs to be added and what needs to be removed
|
// Find what needs to be added and what needs to be removed
|
||||||
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
|
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
|
||||||
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
|
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
|
||||||
|
|
@ -204,10 +236,31 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
return BadRequest("Something went wrong, unable to update user's roles");
|
return BadRequest("Something went wrong, unable to update user's roles");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the API Key assigned with a user
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("reset-api-key")]
|
||||||
|
public async Task<ActionResult<string>> ResetApiKey()
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
user.ApiKey = HashUtil.ApiKey();
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok(user.ApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
return BadRequest("Something went wrong, unable to reset key");
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,16 @@ namespace API.Controllers
|
||||||
private readonly ILogger<BookController> _logger;
|
private readonly ILogger<BookController> _logger;
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
private static readonly string BookApiUrl = "book-resources?file=";
|
private static readonly string BookApiUrl = "book-resources?file=";
|
||||||
|
|
||||||
|
|
||||||
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork)
|
public BookController(ILogger<BookController> logger, IBookService bookService, IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_cacheService = cacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{chapterId}/book-info")]
|
[HttpGet("{chapterId}/book-info")]
|
||||||
|
|
@ -169,9 +171,11 @@ namespace API.Controllers
|
||||||
[HttpGet("{chapterId}/book-page")]
|
[HttpGet("{chapterId}/book-page")]
|
||||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||||
{
|
{
|
||||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
|
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
|
||||||
|
using var book = await EpubReader.OpenBookAsync(path);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
|
|
@ -196,12 +200,7 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
if (doc.ParseErrors.Any())
|
if (doc.ParseErrors.Any())
|
||||||
{
|
{
|
||||||
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
LogBookErrors(book, contentFileRef, doc);
|
||||||
foreach (var error in doc.ParseErrors)
|
|
||||||
{
|
|
||||||
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest("The file is malformed! Cannot read.");
|
return BadRequest("The file is malformed! Cannot read.");
|
||||||
}
|
}
|
||||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||||
|
|
@ -322,5 +321,14 @@ namespace API.Controllers
|
||||||
|
|
||||||
return BadRequest("Could not find the appropriate html for that page");
|
return BadRequest("Could not find the appropriate html for that page");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogBookErrors(EpubBookRef book, EpubTextContentFileRef contentFileRef, HtmlDocument doc)
|
||||||
|
{
|
||||||
|
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
||||||
|
foreach (var error in doc.ParseErrors)
|
||||||
|
{
|
||||||
|
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,25 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// APIs for Collections
|
||||||
|
/// </summary>
|
||||||
public class CollectionController : BaseApiController
|
public class CollectionController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public CollectionController(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
public CollectionController(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a list of all collection tags on the server
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||||
{
|
{
|
||||||
|
|
@ -31,11 +39,17 @@ namespace API.Controllers
|
||||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
{
|
{
|
||||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
}
|
}
|
||||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
|
||||||
|
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryString">Search term</param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("search")]
|
[HttpGet("search")]
|
||||||
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
|
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
|
||||||
|
|
@ -43,20 +57,27 @@ namespace API.Controllers
|
||||||
queryString ??= "";
|
queryString ??= "";
|
||||||
queryString = queryString.Replace(@"%", "");
|
queryString = queryString.Replace(@"%", "");
|
||||||
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
|
|
||||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing tag with a new title, promotion status, and summary.
|
||||||
|
/// <remarks>UI does not contain controls to update title</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updatedTag"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<ActionResult> UpdateTag(CollectionTagDto updatedTag)
|
public async Task<ActionResult> UpdateTagPromotion(CollectionTagDto updatedTag)
|
||||||
{
|
{
|
||||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
|
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
|
||||||
if (existingTag == null) return BadRequest("This tag does not exist");
|
if (existingTag == null) return BadRequest("This tag does not exist");
|
||||||
|
|
||||||
existingTag.Promoted = updatedTag.Promoted;
|
existingTag.Promoted = updatedTag.Promoted;
|
||||||
existingTag.Title = updatedTag.Title;
|
existingTag.Title = updatedTag.Title.Trim();
|
||||||
existingTag.NormalizedTitle = Parser.Parser.Normalize(updatedTag.Title).ToUpper();
|
existingTag.NormalizedTitle = Parser.Parser.Normalize(updatedTag.Title).ToUpper();
|
||||||
|
existingTag.Summary = updatedTag.Summary.Trim();
|
||||||
|
|
||||||
if (_unitOfWork.HasChanges())
|
if (_unitOfWork.HasChanges())
|
||||||
{
|
{
|
||||||
|
|
@ -73,6 +94,11 @@ namespace API.Controllers
|
||||||
return BadRequest("Something went wrong, please try again");
|
return BadRequest("Something went wrong, please try again");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateSeriesForTagDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("update-series")]
|
[HttpPost("update-series")]
|
||||||
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
|
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
|
||||||
|
|
@ -90,6 +116,15 @@ namespace API.Controllers
|
||||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
|
||||||
|
|
||||||
|
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
|
||||||
|
{
|
||||||
|
tag.CoverImageLocked = false;
|
||||||
|
tag.CoverImage = Array.Empty<byte>();
|
||||||
|
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
|
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
|
||||||
{
|
{
|
||||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||||
|
|
@ -101,7 +136,9 @@ namespace API.Controllers
|
||||||
_unitOfWork.CollectionTagRepository.Remove(tag);
|
_unitOfWork.CollectionTagRepository.Remove(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
if (!_unitOfWork.HasChanges()) return Ok("No updates");
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok("Tag updated");
|
return Ok("Tag updated");
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +147,9 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("Something went wrong. Please try again.");
|
return BadRequest("Something went wrong. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Comparators;
|
||||||
|
using API.DTOs.Downloads;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
|
|
@ -21,12 +24,19 @@ namespace API.Controllers
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly IDownloadService _downloadService;
|
||||||
|
private readonly NumericComparer _numericComparer;
|
||||||
|
private const string DefaultContentType = "application/octet-stream";
|
||||||
|
|
||||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService)
|
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, ICacheService cacheService, IDownloadService downloadService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_cacheService = cacheService;
|
||||||
|
_downloadService = downloadService;
|
||||||
|
_numericComparer = new NumericComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("volume-size")]
|
[HttpGet("volume-size")]
|
||||||
|
|
@ -39,7 +49,7 @@ namespace API.Controllers
|
||||||
[HttpGet("chapter-size")]
|
[HttpGet("chapter-size")]
|
||||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||||
{
|
{
|
||||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,15 +64,17 @@ namespace API.Controllers
|
||||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||||
{
|
{
|
||||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeByIdAsync(volumeId);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
if (files.Count == 1)
|
||||||
{
|
{
|
||||||
return await GetFirstFileDownload(files);
|
return await GetFirstFileDownload(files);
|
||||||
}
|
}
|
||||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
$"download_{User.GetUsername()}_v{volumeId}");
|
$"download_{User.GetUsername()}_v{volumeId}");
|
||||||
return File(fileBytes, "application/zip", Path.GetFileNameWithoutExtension(zipPath) + ".zip");
|
return File(fileBytes, DefaultContentType, $"{series.Name} - Volume {volume.Number}.zip");
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
|
|
@ -72,40 +84,26 @@ namespace API.Controllers
|
||||||
|
|
||||||
private async Task<ActionResult> GetFirstFileDownload(IEnumerable<MangaFile> files)
|
private async Task<ActionResult> GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||||
{
|
{
|
||||||
var firstFile = files.Select(c => c.FilePath).First();
|
var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
|
||||||
var fileProvider = new FileExtensionContentTypeProvider();
|
return File(bytes, contentType, fileDownloadName);
|
||||||
// Figures out what the content type should be based on the file name.
|
|
||||||
if (!fileProvider.TryGetContentType(firstFile, out var contentType))
|
|
||||||
{
|
|
||||||
contentType = Path.GetExtension(firstFile).ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
".cbz" => "application/zip",
|
|
||||||
".cbr" => "application/vnd.rar",
|
|
||||||
".cb7" => "application/x-compressed",
|
|
||||||
".epub" => "application/epub+zip",
|
|
||||||
".7z" => "application/x-7z-compressed",
|
|
||||||
".7zip" => "application/x-7z-compressed",
|
|
||||||
".pdf" => "application/pdf",
|
|
||||||
_ => contentType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return File(await _directoryService.ReadFileAsync(firstFile), contentType, Path.GetFileName(firstFile));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("chapter")]
|
[HttpGet("chapter")]
|
||||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||||
{
|
{
|
||||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
if (files.Count == 1)
|
||||||
{
|
{
|
||||||
return await GetFirstFileDownload(files);
|
return await GetFirstFileDownload(files);
|
||||||
}
|
}
|
||||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
$"download_{User.GetUsername()}_c{chapterId}");
|
$"download_{User.GetUsername()}_c{chapterId}");
|
||||||
return File(fileBytes, "application/zip", Path.GetFileNameWithoutExtension(zipPath) + ".zip");
|
return File(fileBytes, DefaultContentType, $"{series.Name} - Chapter {chapter.Number}.zip");
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
|
|
@ -117,20 +115,78 @@ namespace API.Controllers
|
||||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
if (files.Count == 1)
|
||||||
{
|
{
|
||||||
return await GetFirstFileDownload(files);
|
return await GetFirstFileDownload(files);
|
||||||
}
|
}
|
||||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
$"download_{User.GetUsername()}_s{seriesId}");
|
$"download_{User.GetUsername()}_s{seriesId}");
|
||||||
return File(fileBytes, "application/zip", Path.GetFileNameWithoutExtension(zipPath) + ".zip");
|
return File(fileBytes, DefaultContentType, $"{series.Name}.zip");
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("bookmarks")]
|
||||||
|
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||||
|
{
|
||||||
|
// We know that all bookmarks will be for one single seriesId
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||||
|
var totalFilePaths = new List<string>();
|
||||||
|
|
||||||
|
var tempFolder = $"download_{series.Id}_bookmarks";
|
||||||
|
var fullExtractPath = Path.Join(DirectoryService.TempDirectory, tempFolder);
|
||||||
|
if (new DirectoryInfo(fullExtractPath).Exists)
|
||||||
|
{
|
||||||
|
return BadRequest(
|
||||||
|
"Server is currently processing this exact download. Please try again in a few minutes.");
|
||||||
|
}
|
||||||
|
DirectoryService.ExistOrCreate(fullExtractPath);
|
||||||
|
|
||||||
|
var uniqueChapterIds = downloadBookmarkDto.Bookmarks.Select(b => b.ChapterId).Distinct().ToList();
|
||||||
|
|
||||||
|
foreach (var chapterId in uniqueChapterIds)
|
||||||
|
{
|
||||||
|
var chapterExtractPath = Path.Join(fullExtractPath, $"{series.Id}_bookmark_{chapterId}");
|
||||||
|
var chapterPages = downloadBookmarkDto.Bookmarks.Where(b => b.ChapterId == chapterId)
|
||||||
|
.Select(b => b.Page).ToList();
|
||||||
|
var mangaFiles = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
|
switch (series.Format)
|
||||||
|
{
|
||||||
|
case MangaFormat.Image:
|
||||||
|
DirectoryService.ExistOrCreate(chapterExtractPath);
|
||||||
|
_directoryService.CopyFilesToDirectory(mangaFiles.Select(f => f.FilePath), chapterExtractPath, $"{chapterId}_");
|
||||||
|
break;
|
||||||
|
case MangaFormat.Archive:
|
||||||
|
case MangaFormat.Pdf:
|
||||||
|
_cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList());
|
||||||
|
var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath,
|
||||||
|
Parser.Parser.ImageFileExtensions);
|
||||||
|
_directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_");
|
||||||
|
DirectoryService.DeleteFiles(originalFiles);
|
||||||
|
break;
|
||||||
|
case MangaFormat.Epub:
|
||||||
|
return BadRequest("Series is not in a valid format.");
|
||||||
|
default:
|
||||||
|
return BadRequest("Series is not in a valid format. Please rescan series and try again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = _directoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions);
|
||||||
|
// Filter out images that aren't in bookmarks
|
||||||
|
Array.Sort(files, _numericComparer);
|
||||||
|
totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(totalFilePaths,
|
||||||
|
tempFolder);
|
||||||
|
DirectoryService.ClearAndDeleteDirectory(fullExtractPath);
|
||||||
|
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,57 +5,78 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for servicing up images stored in the DB
|
||||||
|
/// </summary>
|
||||||
public class ImageController : BaseApiController
|
public class ImageController : BaseApiController
|
||||||
{
|
{
|
||||||
|
private const string Format = "jpeg";
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ImageController(IUnitOfWork unitOfWork)
|
public ImageController(IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cover image for Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-cover")]
|
[HttpGet("chapter-cover")]
|
||||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||||
{
|
{
|
||||||
var content = await _unitOfWork.VolumeRepository.GetChapterCoverImageAsync(chapterId);
|
var content = await _unitOfWork.VolumeRepository.GetChapterCoverImageAsync(chapterId);
|
||||||
if (content == null) return BadRequest("No cover image");
|
if (content == null) return BadRequest("No cover image");
|
||||||
const string format = "jpeg";
|
|
||||||
|
|
||||||
Response.AddCacheHeader(content);
|
Response.AddCacheHeader(content);
|
||||||
return File(content, "image/" + format, $"chapterId");
|
return File(content, "image/" + Format, $"{chapterId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cover image for Volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("volume-cover")]
|
[HttpGet("volume-cover")]
|
||||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||||
{
|
{
|
||||||
var content = await _unitOfWork.SeriesRepository.GetVolumeCoverImageAsync(volumeId);
|
var content = await _unitOfWork.SeriesRepository.GetVolumeCoverImageAsync(volumeId);
|
||||||
if (content == null) return BadRequest("No cover image");
|
if (content == null) return BadRequest("No cover image");
|
||||||
const string format = "jpeg";
|
|
||||||
|
|
||||||
Response.AddCacheHeader(content);
|
Response.AddCacheHeader(content);
|
||||||
return File(content, "image/" + format, $"volumeId");
|
return File(content, "image/" + Format, $"{volumeId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cover image for Series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId">Id of Series</param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("series-cover")]
|
[HttpGet("series-cover")]
|
||||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||||
{
|
{
|
||||||
var content = await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId);
|
var content = await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId);
|
||||||
if (content == null) return BadRequest("No cover image");
|
if (content == null) return BadRequest("No cover image");
|
||||||
const string format = "jpeg";
|
|
||||||
|
|
||||||
Response.AddCacheHeader(content);
|
Response.AddCacheHeader(content);
|
||||||
return File(content, "image/" + format, $"seriesId");
|
return File(content, "image/" + Format, $"{seriesId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cover image for Collection Tag
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionTagId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("collection-cover")]
|
[HttpGet("collection-cover")]
|
||||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||||
{
|
{
|
||||||
var content = await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId);
|
var content = await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId);
|
||||||
if (content == null) return BadRequest("No cover image");
|
if (content == null) return BadRequest("No cover image");
|
||||||
const string format = "jpeg";
|
|
||||||
|
|
||||||
Response.AddCacheHeader(content);
|
Response.AddCacheHeader(content);
|
||||||
return File(content, "image/" + format, $"collectionTagId");
|
return File(content, "image/" + Format, $"{collectionTagId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,8 @@ namespace API.Controllers
|
||||||
|
|
||||||
if (chapterIds.Any())
|
if (chapterIds.Any())
|
||||||
{
|
{
|
||||||
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
|
@ -203,8 +205,7 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
|
||||||
|
|
||||||
var originalFolders = library.Folders.Select(x => x.Path);
|
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||||
var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders);
|
|
||||||
|
|
||||||
library.Name = libraryForUserDto.Name;
|
library.Name = libraryForUserDto.Name;
|
||||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||||
|
|
@ -212,9 +213,9 @@ namespace API.Controllers
|
||||||
_unitOfWork.LibraryRepository.Update(library);
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
|
||||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||||
if (differenceBetweenFolders.Any())
|
if (originalFolders.Count != libraryForUserDto.Folders.Count())
|
||||||
{
|
{
|
||||||
_taskScheduler.ScanLibrary(library.Id, true);
|
_taskScheduler.ScanLibrary(library.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
|
|
||||||
697
API/Controllers/OPDSController.cs
Normal file
697
API/Controllers/OPDSController.cs
Normal file
|
|
@ -0,0 +1,697 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
using API.Comparators;
|
||||||
|
using API.Constants;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
|
using API.DTOs.OPDS;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
public class OpdsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IDownloadService _downloadService;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly IReaderService _readerService;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly XmlSerializer _xmlSerializer;
|
||||||
|
private readonly XmlSerializer _xmlOpenSearchSerializer;
|
||||||
|
private const string Prefix = "/api/opds/";
|
||||||
|
private readonly FilterDto _filterDto = new FilterDto()
|
||||||
|
{
|
||||||
|
MangaFormat = null
|
||||||
|
};
|
||||||
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
|
|
||||||
|
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||||
|
IDirectoryService directoryService, UserManager<AppUser> userManager,
|
||||||
|
ICacheService cacheService, IReaderService readerService)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_downloadService = downloadService;
|
||||||
|
_directoryService = directoryService;
|
||||||
|
_userManager = userManager;
|
||||||
|
_cacheService = cacheService;
|
||||||
|
_readerService = readerService;
|
||||||
|
|
||||||
|
|
||||||
|
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||||
|
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{apiKey}")]
|
||||||
|
[HttpGet("{apiKey}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> Get(string apiKey)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var feed = CreateFeed("Kavita", string.Empty, apiKey);
|
||||||
|
feed.Id = "root";
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = "inProgress",
|
||||||
|
Title = "In Progress",
|
||||||
|
Content = new FeedEntryContent()
|
||||||
|
{
|
||||||
|
Text = "Browse by In Progress"
|
||||||
|
},
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/in-progress"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = "recentlyAdded",
|
||||||
|
Title = "Recently Added",
|
||||||
|
Content = new FeedEntryContent()
|
||||||
|
{
|
||||||
|
Text = "Browse by Recently Added"
|
||||||
|
},
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/recently-added"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = "allLibraries",
|
||||||
|
Title = "All Libraries",
|
||||||
|
Content = new FeedEntryContent()
|
||||||
|
{
|
||||||
|
Text = "Browse by Libraries"
|
||||||
|
},
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/libraries"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = "allCollections",
|
||||||
|
Title = "All Collections",
|
||||||
|
Content = new FeedEntryContent()
|
||||||
|
{
|
||||||
|
Text = "Browse by Collections"
|
||||||
|
},
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/collections"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/libraries")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetLibraries(string apiKey)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id);
|
||||||
|
|
||||||
|
var feed = CreateFeed("All Libraries", $"{apiKey}/libraries", apiKey);
|
||||||
|
|
||||||
|
foreach (var library in libraries)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = library.Id.ToString(),
|
||||||
|
Title = library.Name,
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/libraries/{library.Id}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/collections")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetCollections(string apiKey)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||||
|
|
||||||
|
IEnumerable <CollectionTagDto> tags;
|
||||||
|
if (isAdmin)
|
||||||
|
{
|
||||||
|
tags = await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var feed = CreateFeed("All Collections", $"{apiKey}/collections", apiKey);
|
||||||
|
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = tag.Id.ToString(),
|
||||||
|
Title = tag.Title,
|
||||||
|
Summary = tag.Summary,
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/collections/{tag.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/collection-cover?collectionId={tag.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/collection-cover?collectionId={tag.Id}")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/collections/{collectionId}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||||
|
|
||||||
|
IEnumerable <CollectionTagDto> tags;
|
||||||
|
if (isAdmin)
|
||||||
|
{
|
||||||
|
tags = await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag = tags.SingleOrDefault(t => t.Id == collectionId);
|
||||||
|
if (tag == null)
|
||||||
|
{
|
||||||
|
return BadRequest("Collection does not exist or you don't have access");
|
||||||
|
}
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, user.Id, new UserParams()
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = 20
|
||||||
|
});
|
||||||
|
|
||||||
|
var feed = CreateFeed(tag.Title + " Collection", $"{apiKey}/collections/{collectionId}", apiKey);
|
||||||
|
AddPagination(feed, series, $"{Prefix}{apiKey}/collections/{collectionId}");
|
||||||
|
|
||||||
|
foreach (var seriesDto in series)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/libraries/{libraryId}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var library =
|
||||||
|
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).SingleOrDefault(l =>
|
||||||
|
l.Id == libraryId);
|
||||||
|
if (library == null)
|
||||||
|
{
|
||||||
|
return BadRequest("User does not have access to this library");
|
||||||
|
}
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, new UserParams()
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = 20
|
||||||
|
}, _filterDto);
|
||||||
|
|
||||||
|
var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey);
|
||||||
|
AddPagination(feed, series, $"{Prefix}{apiKey}/libraries/{libraryId}");
|
||||||
|
|
||||||
|
foreach (var seriesDto in series)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/recently-added")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, user.Id, new UserParams()
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = 20
|
||||||
|
}, _filterDto);
|
||||||
|
|
||||||
|
var feed = CreateFeed("Recently Added", $"{apiKey}/recently-added", apiKey);
|
||||||
|
AddPagination(feed, recentlyAdded, $"{Prefix}{apiKey}/recently-added");
|
||||||
|
|
||||||
|
foreach (var seriesDto in recentlyAdded)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/in-progress")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetInProgress(string apiKey, [FromQuery] int pageNumber = 1)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var userParams = new UserParams()
|
||||||
|
{
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = 20
|
||||||
|
};
|
||||||
|
var results = await _unitOfWork.SeriesRepository.GetInProgress(user.Id, 0, userParams, _filterDto);
|
||||||
|
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||||
|
.Take(userParams.PageSize).ToList();
|
||||||
|
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||||
|
|
||||||
|
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||||
|
|
||||||
|
var feed = CreateFeed("In Progress", $"{apiKey}/in-progress", apiKey);
|
||||||
|
AddPagination(feed, pagedList, $"{Prefix}{apiKey}/in-progress");
|
||||||
|
|
||||||
|
foreach (var seriesDto in pagedList)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/series")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
if (string.IsNullOrEmpty(query))
|
||||||
|
{
|
||||||
|
return BadRequest("You must pass a query parameter");
|
||||||
|
}
|
||||||
|
query = query.Replace(@"%", "");
|
||||||
|
// Get libraries user has access to
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||||
|
|
||||||
|
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.SearchSeries(libraries.Select(l => l.Id).ToArray(), query);
|
||||||
|
|
||||||
|
var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey);
|
||||||
|
|
||||||
|
foreach (var seriesDto in series)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/search")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetSearchDescriptor(string apiKey)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var feed = new OpenSearchDescription()
|
||||||
|
{
|
||||||
|
ShortName = "Search",
|
||||||
|
Description = "Search for Series",
|
||||||
|
Url = new SearchLink()
|
||||||
|
{
|
||||||
|
Type = FeedLinkType.AtomAcquisition,
|
||||||
|
Template = $"{Prefix}{apiKey}/series?query=" + "{searchTerms}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var sm = new StringWriter();
|
||||||
|
_xmlOpenSearchSerializer.Serialize(sm, feed);
|
||||||
|
|
||||||
|
return CreateXmlResult(sm.ToString().Replace("utf-16", "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/series/{seriesId}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetSeries(string apiKey, int seriesId)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||||
|
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
|
||||||
|
var feed = CreateFeed(series.Name + " - Volumes", $"{apiKey}/series/{series.Id}", apiKey);
|
||||||
|
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesId}"));
|
||||||
|
foreach (var volumeDto in volumes)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateVolume(volumeDto, seriesId, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetVolume(string apiKey, int seriesId, int volumeId)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||||
|
var chapters =
|
||||||
|
(await _unitOfWork.VolumeRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||||
|
_chapterSortComparer);
|
||||||
|
|
||||||
|
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + " - Chapters ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey);
|
||||||
|
foreach (var chapter in chapters)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = chapter.Id.ToString(),
|
||||||
|
Title = "Chapter " + chapter.Number,
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapter.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapter.Id}")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
public async Task<IActionResult> GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var user = await GetUser(apiKey);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||||
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterDtoAsync(chapterId);
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
|
|
||||||
|
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + " - Chapters ", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey);
|
||||||
|
foreach (var mangaFile in files)
|
||||||
|
{
|
||||||
|
feed.Entries.Add(CreateChapter(seriesId, volumeId, chapterId, mangaFile, series, volume, chapter, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads a file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="filename">Not used. Only for Chunky to allow download links</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
|
||||||
|
public async Task<ActionResult> DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
|
||||||
|
{
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
|
var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
|
||||||
|
return File(bytes, contentType, fileDownloadName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentResult CreateXmlResult(string xml)
|
||||||
|
{
|
||||||
|
return new ContentResult
|
||||||
|
{
|
||||||
|
ContentType = "application/xml",
|
||||||
|
Content = xml,
|
||||||
|
StatusCode = 200
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPagination(Feed feed, PagedList<SeriesDto> list, string href)
|
||||||
|
{
|
||||||
|
var url = href;
|
||||||
|
if (href.Contains("?"))
|
||||||
|
{
|
||||||
|
url += "&";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
url += "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageNumber = Math.Max(list.CurrentPage, 1);
|
||||||
|
|
||||||
|
if (pageNumber > 1)
|
||||||
|
{
|
||||||
|
feed.Links.Add(CreateLink(FeedLinkRelation.Prev, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNumber + 1 < list.TotalPages)
|
||||||
|
{
|
||||||
|
feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update self to point to current page
|
||||||
|
var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self);
|
||||||
|
if (selfLink != null)
|
||||||
|
{
|
||||||
|
selfLink.Href = url + "pageNumber=" + pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
feed.Total = list.TotalPages * list.PageSize;
|
||||||
|
feed.ItemsPerPage = list.PageSize;
|
||||||
|
feed.StartIndex = (Math.Max(list.CurrentPage - 1, 0) * list.PageSize) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FeedEntry CreateSeries(SeriesDto seriesDto, string apiKey)
|
||||||
|
{
|
||||||
|
return new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = seriesDto.Id.ToString(),
|
||||||
|
Title = $"{seriesDto.Name} ({seriesDto.Format})",
|
||||||
|
Summary = seriesDto.Summary,
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesDto.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesDto.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesDto.Id}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FeedEntry CreateSeries(SearchResultDto searchResultDto, string apiKey)
|
||||||
|
{
|
||||||
|
return new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = searchResultDto.SeriesId.ToString(),
|
||||||
|
Title = $"{searchResultDto.Name} ({searchResultDto.Format})",
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{searchResultDto.SeriesId}"),
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={searchResultDto.SeriesId}"),
|
||||||
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/series-cover?seriesId={searchResultDto.SeriesId}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FeedEntry CreateVolume(VolumeDto volumeDto, int seriesId, string apiKey)
|
||||||
|
{
|
||||||
|
return new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = volumeDto.Id.ToString(),
|
||||||
|
Title = volumeDto.IsSpecial ? "Specials" : "Volume " + volumeDto.Name,
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeDto.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/volume-cover?volumeId={volumeDto.Id}"),
|
||||||
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/volume-cover?volumeId={volumeDto.Id}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private FeedEntry CreateChapter(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, Volume volume, ChapterDto chapter, string apiKey)
|
||||||
|
{
|
||||||
|
var fileSize =
|
||||||
|
DirectoryService.GetHumanReadableBytes(DirectoryService.GetTotalSize(new List<string>()
|
||||||
|
{mangaFile.FilePath}));
|
||||||
|
var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath);
|
||||||
|
var filename = Uri.EscapeUriString(Path.GetFileName(mangaFile.FilePath) ?? string.Empty);
|
||||||
|
return new FeedEntry()
|
||||||
|
{
|
||||||
|
Id = mangaFile.Id.ToString(),
|
||||||
|
Title = $"{series.Name} - Volume {volume.Name} - Chapter {chapter.Number}",
|
||||||
|
Extent = fileSize,
|
||||||
|
Summary = $"{fileType.Split("/")[1]} - {fileSize}",
|
||||||
|
Format = mangaFile.Format.ToString(),
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||||
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||||
|
// Chunky requires a file at the end. Our API ignores this
|
||||||
|
CreateLink(FeedLinkRelation.Acquisition, fileType, $"{Prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}"),
|
||||||
|
CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey)
|
||||||
|
},
|
||||||
|
Content = new FeedEntryContent()
|
||||||
|
{
|
||||||
|
Text = fileType,
|
||||||
|
Type = "text"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/image")]
|
||||||
|
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
|
||||||
|
{
|
||||||
|
if (pageNumber < 0) return BadRequest("Page cannot be less than 0");
|
||||||
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
|
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (path, _) = await _cacheService.GetCachedPagePath(chapter, pageNumber);
|
||||||
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}");
|
||||||
|
|
||||||
|
var content = await _directoryService.ReadFileAsync(path);
|
||||||
|
var format = Path.GetExtension(path).Replace(".", "");
|
||||||
|
|
||||||
|
// Calculates SHA1 Hash for byte[]
|
||||||
|
Response.AddCacheHeader(content);
|
||||||
|
|
||||||
|
// Save progress for the user
|
||||||
|
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||||
|
{
|
||||||
|
ChapterId = chapterId,
|
||||||
|
PageNum = pageNumber,
|
||||||
|
SeriesId = seriesId,
|
||||||
|
VolumeId = volumeId
|
||||||
|
}, await GetUser(apiKey));
|
||||||
|
|
||||||
|
return File(content, "image/" + format);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_cacheService.CleanupChapters(new []{ chapterId });
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{apiKey}/favicon")]
|
||||||
|
public async Task<ActionResult> GetFavicon(string apiKey)
|
||||||
|
{
|
||||||
|
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
|
||||||
|
if (files.Length == 0) return BadRequest("Cannot find icon");
|
||||||
|
var path = files[0];
|
||||||
|
var content = await _directoryService.ReadFileAsync(path);
|
||||||
|
var format = Path.GetExtension(path).Replace(".", "");
|
||||||
|
|
||||||
|
// Calculates SHA1 Hash for byte[]
|
||||||
|
Response.AddCacheHeader(content);
|
||||||
|
|
||||||
|
return File(content, "image/" + format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user from the API key
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<AppUser> GetUser(string apiKey)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByApiKeyAsync(apiKey);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new KavitaException("User does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FeedLink CreatePageStreamLink(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey)
|
||||||
|
{
|
||||||
|
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg", $"{Prefix}{apiKey}/image?seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
|
||||||
|
link.TotalPages = mangaFile.Pages;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FeedLink CreateLink(string rel, string type, string href)
|
||||||
|
{
|
||||||
|
return new FeedLink()
|
||||||
|
{
|
||||||
|
Rel = rel,
|
||||||
|
Href = href,
|
||||||
|
Type = type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Feed CreateFeed(string title, string href, string apiKey)
|
||||||
|
{
|
||||||
|
var link = CreateLink(FeedLinkRelation.Self, string.IsNullOrEmpty(href) ?
|
||||||
|
FeedLinkType.AtomNavigation :
|
||||||
|
FeedLinkType.AtomAcquisition, Prefix + href);
|
||||||
|
|
||||||
|
return new Feed()
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Icon = Prefix + $"{apiKey}/favicon",
|
||||||
|
Links = new List<FeedLink>()
|
||||||
|
{
|
||||||
|
link,
|
||||||
|
CreateLink(FeedLinkRelation.Start, FeedLinkType.AtomNavigation, Prefix + apiKey),
|
||||||
|
CreateLink(FeedLinkRelation.Search, FeedLinkType.AtomSearch, Prefix + $"{apiKey}/search")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SerializeXml(Feed feed)
|
||||||
|
{
|
||||||
|
if (feed == null) return string.Empty;
|
||||||
|
using var sm = new StringWriter();
|
||||||
|
_xmlSerializer.Serialize(sm, feed);
|
||||||
|
return sm.ToString().Replace("utf-16", "utf-8"); // Chunky cannot accept UTF-16 feeds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,25 +11,41 @@ using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// For all things regarding reading, mainly focusing on non-Book related entities
|
||||||
|
/// </summary>
|
||||||
public class ReaderController : BaseApiController
|
public class ReaderController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ILogger<ReaderController> _logger;
|
||||||
|
private readonly IReaderService _readerService;
|
||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||||
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||||
|
|
||||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
/// <inheritdoc />
|
||||||
|
public ReaderController(IDirectoryService directoryService, ICacheService cacheService,
|
||||||
|
IUnitOfWork unitOfWork, ILogger<ReaderController> logger, IReaderService readerService)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_logger = logger;
|
||||||
|
_readerService = readerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("image")]
|
[HttpGet("image")]
|
||||||
public async Task<ActionResult> GetImage(int chapterId, int page)
|
public async Task<ActionResult> GetImage(int chapterId, int page)
|
||||||
{
|
{
|
||||||
|
|
@ -37,28 +53,43 @@ namespace API.Controllers
|
||||||
var chapter = await _cacheService.Ensure(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||||
|
|
||||||
var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
|
try
|
||||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
{
|
||||||
|
var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
|
||||||
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||||
|
|
||||||
var content = await _directoryService.ReadFileAsync(path);
|
var content = await _directoryService.ReadFileAsync(path);
|
||||||
var format = Path.GetExtension(path).Replace(".", "");
|
var format = Path.GetExtension(path).Replace(".", "");
|
||||||
|
|
||||||
// Calculates SHA1 Hash for byte[]
|
// Calculates SHA1 Hash for byte[]
|
||||||
Response.AddCacheHeader(content);
|
Response.AddCacheHeader(content);
|
||||||
|
|
||||||
return File(content, "image/" + format);
|
return File(content, "image/" + format);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_cacheService.CleanupChapters(new []{ chapterId });
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-info")]
|
[HttpGet("chapter-info")]
|
||||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
|
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
||||||
{
|
{
|
||||||
// PERF: Write this in one DB call
|
// PERF: Write this in one DB call
|
||||||
var chapter = await _cacheService.Ensure(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
||||||
if (volume == null) return BadRequest("Could not find Volume");
|
if (volume == null) return BadRequest("Could not find Volume");
|
||||||
var (_, mangaFile) = await _cacheService.GetCachedPagePath(chapter, 0);
|
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
|
|
||||||
return Ok(new ChapterInfoDto()
|
return Ok(new ChapterInfoDto()
|
||||||
{
|
{
|
||||||
|
|
@ -72,29 +103,6 @@ namespace API.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("get-bookmark")]
|
|
||||||
public async Task<ActionResult<BookmarkDto>> GetBookmark(int chapterId)
|
|
||||||
{
|
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
||||||
var bookmark = new BookmarkDto()
|
|
||||||
{
|
|
||||||
PageNum = 0,
|
|
||||||
ChapterId = chapterId,
|
|
||||||
VolumeId = 0,
|
|
||||||
SeriesId = 0
|
|
||||||
};
|
|
||||||
if (user.Progresses == null) return Ok(bookmark);
|
|
||||||
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
|
||||||
|
|
||||||
if (progress != null)
|
|
||||||
{
|
|
||||||
bookmark.SeriesId = progress.SeriesId;
|
|
||||||
bookmark.VolumeId = progress.VolumeId;
|
|
||||||
bookmark.PageNum = progress.PagesRead;
|
|
||||||
bookmark.BookScrollId = progress.BookScrollId;
|
|
||||||
}
|
|
||||||
return Ok(bookmark);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("mark-read")]
|
[HttpPost("mark-read")]
|
||||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||||
|
|
@ -106,8 +114,9 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
foreach (var chapter in volume.Chapters)
|
foreach (var chapter in volume.Chapters)
|
||||||
{
|
{
|
||||||
var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
if (userProgress == null) // I need to get all chapters and generate new user progresses for them?
|
|
||||||
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
user.Progresses.Add(new AppUserProgress
|
user.Progresses.Add(new AppUserProgress
|
||||||
{
|
{
|
||||||
|
|
@ -137,6 +146,36 @@ namespace API.Controllers
|
||||||
return BadRequest("There was an issue saving progress");
|
return BadRequest("There was an issue saving progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||||
|
{
|
||||||
|
AppUserProgress userProgress = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userProgress =
|
||||||
|
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
|
||||||
|
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||||
|
if (progresses.Count > 1)
|
||||||
|
{
|
||||||
|
user.Progresses = new List<AppUserProgress>()
|
||||||
|
{
|
||||||
|
user.Progresses.First()
|
||||||
|
};
|
||||||
|
userProgress = user.Progresses.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a Chapter as Unread (progress)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="markReadDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("mark-unread")]
|
[HttpPost("mark-unread")]
|
||||||
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
||||||
{
|
{
|
||||||
|
|
@ -147,23 +186,12 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
foreach (var chapter in volume.Chapters)
|
foreach (var chapter in volume.Chapters)
|
||||||
{
|
{
|
||||||
var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
if (userProgress == null)
|
|
||||||
{
|
if (userProgress == null) continue;
|
||||||
user.Progresses.Add(new AppUserProgress
|
userProgress.PagesRead = 0;
|
||||||
{
|
userProgress.SeriesId = markReadDto.SeriesId;
|
||||||
PagesRead = 0,
|
userProgress.VolumeId = volume.Id;
|
||||||
VolumeId = volume.Id,
|
|
||||||
SeriesId = markReadDto.SeriesId,
|
|
||||||
ChapterId = chapter.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userProgress.PagesRead = 0;
|
|
||||||
userProgress.SeriesId = markReadDto.SeriesId;
|
|
||||||
userProgress.VolumeId = volume.Id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,6 +206,11 @@ namespace API.Controllers
|
||||||
return BadRequest("There was an issue saving progress");
|
return BadRequest("There was an issue saving progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all chapters within a volume as Read
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="markVolumeReadDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("mark-volume-read")]
|
[HttpPost("mark-volume-read")]
|
||||||
public async Task<ActionResult> MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto)
|
public async Task<ActionResult> MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto)
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +220,7 @@ namespace API.Controllers
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
||||||
|
|
||||||
if (userProgress == null)
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
|
|
@ -217,49 +250,175 @@ namespace API.Controllers
|
||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns Progress (page number) for a chapter for the logged in user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-progress")]
|
||||||
|
public async Task<ActionResult<ProgressDto>> GetProgress(int chapterId)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
var progressBookmark = new ProgressDto()
|
||||||
|
{
|
||||||
|
PageNum = 0,
|
||||||
|
ChapterId = chapterId,
|
||||||
|
VolumeId = 0,
|
||||||
|
SeriesId = 0
|
||||||
|
};
|
||||||
|
if (user.Progresses == null) return Ok(progressBookmark);
|
||||||
|
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
||||||
|
|
||||||
|
if (progress != null)
|
||||||
|
{
|
||||||
|
progressBookmark.SeriesId = progress.SeriesId;
|
||||||
|
progressBookmark.VolumeId = progress.VolumeId;
|
||||||
|
progressBookmark.PageNum = progress.PagesRead;
|
||||||
|
progressBookmark.BookScrollId = progress.BookScrollId;
|
||||||
|
}
|
||||||
|
return Ok(progressBookmark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save page against Chapter for logged in user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progressDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("progress")]
|
||||||
|
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (await _readerService.SaveReadingProgress(progressDto, user)) return Ok(true);
|
||||||
|
|
||||||
|
return BadRequest("Could not save progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of bookmarked pages for a given Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-bookmarks")]
|
||||||
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all bookmarked pages for a User
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-all-bookmarks")]
|
||||||
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all bookmarks for all chapters linked to a Series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("remove-bookmarks")]
|
||||||
|
public async Task<ActionResult> RemoveBookmarks(RemoveBookmarkForSeriesDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList();
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when trying to clear bookmarks");
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Could not clear bookmarks");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all bookmarked pages for a given volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-volume-bookmarks")]
|
||||||
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all bookmarked pages for a given series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-series-bookmarks")]
|
||||||
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
|
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bookmarks a page against a Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookmarkDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("bookmark")]
|
[HttpPost("bookmark")]
|
||||||
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
// Don't let user bookmark past total pages.
|
// Don't let user save past total pages.
|
||||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
||||||
if (bookmarkDto.PageNum > chapter.Pages)
|
if (bookmarkDto.Page > chapter.Pages)
|
||||||
{
|
{
|
||||||
return BadRequest("Can't bookmark past max pages");
|
bookmarkDto.Page = chapter.Pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bookmarkDto.PageNum < 0)
|
if (bookmarkDto.Page < 0)
|
||||||
{
|
{
|
||||||
return BadRequest("Can't bookmark less than 0");
|
bookmarkDto.Page = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
user.Bookmarks ??= new List<AppUserBookmark>();
|
||||||
var userProgress =
|
var userBookmark =
|
||||||
user.Progresses.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id);
|
user.Bookmarks.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id && x.Page == bookmarkDto.Page);
|
||||||
|
|
||||||
if (userProgress == null)
|
if (userBookmark == null)
|
||||||
{
|
{
|
||||||
user.Progresses.Add(new AppUserProgress
|
user.Bookmarks.Add(new AppUserBookmark()
|
||||||
{
|
{
|
||||||
PagesRead = bookmarkDto.PageNum,
|
Page = bookmarkDto.Page,
|
||||||
VolumeId = bookmarkDto.VolumeId,
|
VolumeId = bookmarkDto.VolumeId,
|
||||||
SeriesId = bookmarkDto.SeriesId,
|
SeriesId = bookmarkDto.SeriesId,
|
||||||
ChapterId = bookmarkDto.ChapterId,
|
ChapterId = bookmarkDto.ChapterId,
|
||||||
BookScrollId = bookmarkDto.BookScrollId,
|
|
||||||
LastModified = DateTime.Now
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProgress.PagesRead = bookmarkDto.PageNum;
|
userBookmark.Page = bookmarkDto.Page;
|
||||||
userProgress.SeriesId = bookmarkDto.SeriesId;
|
userBookmark.SeriesId = bookmarkDto.SeriesId;
|
||||||
userProgress.VolumeId = bookmarkDto.VolumeId;
|
userBookmark.VolumeId = bookmarkDto.VolumeId;
|
||||||
userProgress.BookScrollId = bookmarkDto.BookScrollId;
|
|
||||||
userProgress.LastModified = DateTime.Now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
@ -274,7 +433,40 @@ namespace API.Controllers
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save bookmark");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a bookmarked page for a Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookmarkDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("unbookmark")]
|
||||||
|
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
if (user.Bookmarks == null) return Ok();
|
||||||
|
try {
|
||||||
|
user.Bookmarks = user.Bookmarks.Where(x =>
|
||||||
|
x.ChapterId == bookmarkDto.ChapterId
|
||||||
|
&& x.AppUserId == user.Id
|
||||||
|
&& x.Page != bookmarkDto.Page).ToList();
|
||||||
|
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Could not remove bookmark");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -27,12 +29,12 @@ namespace API.Controllers
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpPost]
|
||||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var series =
|
var series =
|
||||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams);
|
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams, filterDto);
|
||||||
|
|
||||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||||
if (series == null) return BadRequest("Could not get series for library");
|
if (series == null) return BadRequest("Could not get series for library");
|
||||||
|
|
@ -44,11 +46,26 @@ namespace API.Controllers
|
||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches a Series for a given Id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId">Series Id to fetch details for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="KavitaException">Throws an exception if the series Id does exist</exception>
|
||||||
[HttpGet("{seriesId}")]
|
[HttpGet("{seriesId}")]
|
||||||
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
|
||||||
|
throw new KavitaException("This series does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
|
@ -62,6 +79,9 @@ namespace API.Controllers
|
||||||
|
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||||
|
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
|
|
@ -119,7 +139,7 @@ namespace API.Controllers
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("update")]
|
||||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
||||||
|
|
@ -132,27 +152,39 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
|
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
|
||||||
}
|
}
|
||||||
series.Name = updateSeries.Name;
|
series.Name = updateSeries.Name.Trim();
|
||||||
series.LocalizedName = updateSeries.LocalizedName;
|
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
||||||
series.SortName = updateSeries.SortName;
|
series.SortName = updateSeries.SortName?.Trim();
|
||||||
series.Summary = updateSeries.Summary;
|
series.Summary = updateSeries.Summary?.Trim();
|
||||||
|
|
||||||
|
var needsRefreshMetadata = false;
|
||||||
|
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
|
||||||
|
{
|
||||||
|
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||||
|
needsRefreshMetadata = true;
|
||||||
|
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
||||||
|
}
|
||||||
|
|
||||||
_unitOfWork.SeriesRepository.Update(series);
|
_unitOfWork.SeriesRepository.Update(series);
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
|
if (needsRefreshMetadata)
|
||||||
|
{
|
||||||
|
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
|
||||||
|
}
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest("There was an error with updating the series");
|
return BadRequest("There was an error with updating the series");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("recently-added")]
|
[HttpPost("recently-added")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded([FromQuery] UserParams userParams, int libraryId = 0)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var series =
|
var series =
|
||||||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams);
|
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams, filterDto);
|
||||||
|
|
||||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||||
if (series == null) return BadRequest("Could not get series");
|
if (series == null) return BadRequest("Could not get series");
|
||||||
|
|
@ -164,12 +196,20 @@ namespace API.Controllers
|
||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("in-progress")]
|
[HttpPost("in-progress")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(int libraryId = 0, int limit = 20)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
|
// NOTE: This has to be done manually like this due to the DistinctBy requirement
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null) return Ok(Array.Empty<SeriesDto>());
|
var results = await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, userParams, filterDto);
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, limit));
|
|
||||||
|
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||||
|
.Take(userParams.PageSize).ToList();
|
||||||
|
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||||
|
|
||||||
|
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||||
|
|
||||||
|
return Ok(pagedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
|
@ -202,6 +242,7 @@ namespace API.Controllers
|
||||||
{
|
{
|
||||||
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
|
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
|
var allTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList();
|
||||||
if (series.Metadata == null)
|
if (series.Metadata == null)
|
||||||
{
|
{
|
||||||
series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags
|
series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags
|
||||||
|
|
@ -226,13 +267,13 @@ namespace API.Controllers
|
||||||
// At this point, all tags that aren't in dto have been removed.
|
// At this point, all tags that aren't in dto have been removed.
|
||||||
foreach (var tag in updateSeriesMetadataDto.Tags)
|
foreach (var tag in updateSeriesMetadataDto.Tags)
|
||||||
{
|
{
|
||||||
var existingTag = series.Metadata.CollectionTags.SingleOrDefault(t => t.Title == tag.Title);
|
var existingTag = allTags.SingleOrDefault(t => t.Title == tag.Title);
|
||||||
if (existingTag != null)
|
if (existingTag != null)
|
||||||
{
|
{
|
||||||
// Update existingTag
|
if (!series.Metadata.CollectionTags.Any(t => t.Title == tag.Title))
|
||||||
existingTag.Promoted = tag.Promoted;
|
{
|
||||||
existingTag.Title = tag.Title;
|
newTags.Add(existingTag);
|
||||||
existingTag.NormalizedTitle = Parser.Parser.Normalize(tag.Title).ToUpper();
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -257,14 +298,21 @@ namespace API.Controllers
|
||||||
return Ok("Successfully updated");
|
return Ok("Successfully updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when updating metadata");
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest("Could not update metadata");
|
return BadRequest("Could not update metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all Series grouped by the passed Collection Id with Pagination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionId">Collection Id to pull series from</param>
|
||||||
|
/// <param name="userParams">Pagination information</param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("series-by-collection")]
|
[HttpGet("series-by-collection")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
|
||||||
{
|
{
|
||||||
|
|
@ -282,6 +330,19 @@ namespace API.Controllers
|
||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
|
||||||
|
/// the user does not have access to.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("series-by-ids")]
|
||||||
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||||
|
{
|
||||||
|
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, user.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs.Stats;
|
using API.DTOs.Stats;
|
||||||
|
using API.DTOs.Update;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
|
@ -23,9 +25,11 @@ namespace API.Controllers
|
||||||
private readonly IBackupService _backupService;
|
private readonly IBackupService _backupService;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||||
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService)
|
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService,
|
||||||
|
IVersionUpdaterService versionUpdaterService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
@ -33,8 +37,13 @@ namespace API.Controllers
|
||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
|
_versionUpdaterService = versionUpdaterService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to Restart the server. Does not work, will shutdown the instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("restart")]
|
[HttpPost("restart")]
|
||||||
public ActionResult RestartServer()
|
public ActionResult RestartServer()
|
||||||
{
|
{
|
||||||
|
|
@ -44,6 +53,10 @@ namespace API.Controllers
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an ad-hoc cleanup of Cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("clear-cache")]
|
[HttpPost("clear-cache")]
|
||||||
public ActionResult ClearCache()
|
public ActionResult ClearCache()
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +66,19 @@ namespace API.Controllers
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an ad-hoc backup of the Database
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("backup-db")]
|
||||||
|
public ActionResult BackupDatabase()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
||||||
|
_backupService.BackupDatabase();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns non-sensitive information about the current system
|
/// Returns non-sensitive information about the current system
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -78,6 +104,16 @@ namespace API.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("check-update")]
|
||||||
|
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||||
|
{
|
||||||
|
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("changelog")]
|
||||||
|
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||||
|
{
|
||||||
|
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
public class SettingsController : BaseApiController
|
public class SettingsController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
|
|
@ -30,6 +29,7 @@ namespace API.Controllers
|
||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
{
|
{
|
||||||
|
|
@ -39,6 +39,7 @@ namespace API.Controllers
|
||||||
return Ok(settingsDto);
|
return Ok(settingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||||
{
|
{
|
||||||
|
|
@ -86,6 +87,12 @@ namespace API.Controllers
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_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.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||||
|
|
@ -114,22 +121,32 @@ namespace API.Controllers
|
||||||
return Ok(updateSettingsDto);
|
return Ok(updateSettingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("task-frequencies")]
|
[HttpGet("task-frequencies")]
|
||||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||||
{
|
{
|
||||||
return Ok(CronConverter.Options);
|
return Ok(CronConverter.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("library-types")]
|
[HttpGet("library-types")]
|
||||||
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
||||||
{
|
{
|
||||||
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
|
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("log-levels")]
|
[HttpGet("log-levels")]
|
||||||
public ActionResult<IEnumerable<string>> GetLogLevels()
|
public ActionResult<IEnumerable<string>> GetLogLevels()
|
||||||
{
|
{
|
||||||
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("opds-enabled")]
|
||||||
|
public async Task<ActionResult<bool>> GetOpdsEnabled()
|
||||||
|
{
|
||||||
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
return Ok(settingsDto.EnableOpds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,11 @@ namespace API.Controllers
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Error updating the usage statistics");
|
_logger.LogError(ex, "Error updating the usage statistics");
|
||||||
Console.WriteLine(e);
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
208
API/Controllers/UploadController.cs
Normal file
208
API/Controllers/UploadController.cs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Uploads;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
public class UploadController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IImageService _imageService;
|
||||||
|
private readonly ILogger<UploadController> _logger;
|
||||||
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger, ITaskScheduler taskScheduler)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_imageService = imageService;
|
||||||
|
_logger = logger;
|
||||||
|
_taskScheduler = taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces series cover image and locks it with a base64 encoded image
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uploadFileDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[RequestSizeLimit(8_000_000)]
|
||||||
|
[HttpPost("series")]
|
||||||
|
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(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
|
||||||
|
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||||
|
{
|
||||||
|
return BadRequest("You must pass a url to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||||
|
|
||||||
|
if (bytes.Length > 0)
|
||||||
|
{
|
||||||
|
series.CoverImage = bytes;
|
||||||
|
series.CoverImageLocked = true;
|
||||||
|
_unitOfWork.SeriesRepository.Update(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Unable to save cover image to Series");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces collection tag cover image and locks it with a base64 encoded image
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uploadFileDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[RequestSizeLimit(8_000_000)]
|
||||||
|
[HttpPost("collection")]
|
||||||
|
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(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
|
||||||
|
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||||
|
{
|
||||||
|
return BadRequest("You must pass a url to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||||
|
|
||||||
|
if (bytes.Length > 0)
|
||||||
|
{
|
||||||
|
tag.CoverImage = bytes;
|
||||||
|
tag.CoverImageLocked = true;
|
||||||
|
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Unable to save cover image to Collection Tag");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uploadFileDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[RequestSizeLimit(8_000_000)]
|
||||||
|
[HttpPost("chapter")]
|
||||||
|
public async Task<ActionResult> UploadChapterCoverImageFromUrl(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
|
||||||
|
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||||
|
{
|
||||||
|
return BadRequest("You must pass a url to use");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||||
|
|
||||||
|
if (bytes.Length > 0)
|
||||||
|
{
|
||||||
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
|
||||||
|
chapter.CoverImage = bytes;
|
||||||
|
chapter.CoverImageLocked = true;
|
||||||
|
_unitOfWork.ChapterRepository.Update(chapter);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
||||||
|
volume.CoverImage = chapter.CoverImage;
|
||||||
|
_unitOfWork.VolumeRepository.Update(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Unable to save cover image to Chapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uploadFileDto">Does not use Url property</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("reset-chapter-lock")]
|
||||||
|
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
|
||||||
|
chapter.CoverImage = Array.Empty<byte>();
|
||||||
|
chapter.CoverImageLocked = false;
|
||||||
|
_unitOfWork.ChapterRepository.Update(chapter);
|
||||||
|
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
||||||
|
volume.CoverImage = chapter.CoverImage;
|
||||||
|
_unitOfWork.VolumeRepository.Update(volume);
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,10 @@
|
||||||
{
|
{
|
||||||
public class BookmarkDto
|
public class BookmarkDto
|
||||||
{
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
public int ChapterId { get; set; }
|
|
||||||
public int PageNum { get; set; }
|
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
/// <summary>
|
public int ChapterId { get; set; }
|
||||||
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
|
|
||||||
/// on pages that combine multiple "chapters".
|
|
||||||
/// </summary>
|
|
||||||
public string BookScrollId { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
public class ChapterDto
|
public class ChapterDto
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
|
|
@ -10,7 +14,7 @@ namespace API.DTOs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Range { get; init; }
|
public string Range { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Smallest number of the Range.
|
/// Smallest number of the Range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Number { get; init; }
|
public string Number { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -22,7 +26,7 @@ namespace API.DTOs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSpecial { get; init; }
|
public bool IsSpecial { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
|
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; init; }
|
public string Title { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -33,6 +37,13 @@ namespace API.DTOs
|
||||||
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
|
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PagesRead { get; set; }
|
public int PagesRead { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the Cover Image is locked for this entity
|
||||||
|
/// </summary>
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Volume Id this Chapter belongs to
|
||||||
|
/// </summary>
|
||||||
public int VolumeId { get; init; }
|
public int VolumeId { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
public bool Promoted { get; set; }
|
public bool Promoted { get; set; }
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
API/DTOs/Downloads/DownloadBookmarkDto.cs
Normal file
9
API/DTOs/Downloads/DownloadBookmarkDto.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.Downloads
|
||||||
|
{
|
||||||
|
public class DownloadBookmarkDto
|
||||||
|
{
|
||||||
|
public IEnumerable<BookmarkDto> Bookmarks { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
API/DTOs/Filtering/FilterDto.cs
Normal file
13
API/DTOs/Filtering/FilterDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs.Filtering
|
||||||
|
{
|
||||||
|
public class FilterDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pass null if you want all formats
|
||||||
|
/// </summary>
|
||||||
|
public MangaFormat? MangaFormat { get; init; } = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
12
API/DTOs/OPDS/Author.cs
Normal file
12
API/DTOs/OPDS/Author.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public class Author
|
||||||
|
{
|
||||||
|
[XmlElement("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[XmlElement("uri")]
|
||||||
|
public string Uri { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
62
API/DTOs/OPDS/Feed.cs
Normal file
62
API/DTOs/OPDS/Feed.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")]
|
||||||
|
public class Feed
|
||||||
|
{
|
||||||
|
[XmlElement("updated")]
|
||||||
|
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
|
||||||
|
|
||||||
|
[XmlElement("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("icon")]
|
||||||
|
public string Icon { get; set; } = "/favicon.ico";
|
||||||
|
|
||||||
|
[XmlElement("author")]
|
||||||
|
public Author Author { get; set; } = new Author()
|
||||||
|
{
|
||||||
|
Name = "Kavita",
|
||||||
|
Uri = "https://kavitareader.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
[XmlElement("totalResults", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
|
||||||
|
public int? Total { get; set; } = null;
|
||||||
|
|
||||||
|
[XmlElement("itemsPerPage", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
|
||||||
|
public int? ItemsPerPage { get; set; } = null;
|
||||||
|
|
||||||
|
[XmlElement("startIndex", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
|
||||||
|
public int? StartIndex { get; set; } = null;
|
||||||
|
|
||||||
|
[XmlElement("link")]
|
||||||
|
public List<FeedLink> Links { get; set; } = new List<FeedLink>() ;
|
||||||
|
|
||||||
|
[XmlElement("entry")]
|
||||||
|
public List<FeedEntry> Entries { get; set; } = new List<FeedEntry>();
|
||||||
|
|
||||||
|
public bool ShouldSerializeTotal()
|
||||||
|
{
|
||||||
|
return Total.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldSerializeItemsPerPage()
|
||||||
|
{
|
||||||
|
return ItemsPerPage.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldSerializeStartIndex()
|
||||||
|
{
|
||||||
|
return StartIndex.HasValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
API/DTOs/OPDS/FeedEntry.cs
Normal file
51
API/DTOs/OPDS/FeedEntry.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public class FeedEntry
|
||||||
|
{
|
||||||
|
[XmlElement("updated")]
|
||||||
|
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
|
||||||
|
|
||||||
|
[XmlElement("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("summary")]
|
||||||
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents Size of the Entry
|
||||||
|
/// Tag: , ElementName = "dcterms:extent"
|
||||||
|
/// <example>2 MB</example>
|
||||||
|
/// </summary>
|
||||||
|
[XmlElement("extent", Namespace = "http://purl.org/dc/terms/")]
|
||||||
|
public string Extent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format of the file
|
||||||
|
/// https://dublincore.org/specifications/dublin-core/dcmi-terms/
|
||||||
|
/// </summary>
|
||||||
|
[XmlElement("format", Namespace = "http://purl.org/dc/terms/format")]
|
||||||
|
public string Format { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("language", Namespace = "http://purl.org/dc/terms/")]
|
||||||
|
public string Language { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("content")]
|
||||||
|
public FeedEntryContent Content { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("link")]
|
||||||
|
public List<FeedLink> Links = new List<FeedLink>();
|
||||||
|
|
||||||
|
// [XmlElement("author")]
|
||||||
|
// public List<FeedAuthor> Authors = new List<FeedAuthor>();
|
||||||
|
|
||||||
|
// [XmlElement("category")]
|
||||||
|
// public List<FeedCategory> Categories = new List<FeedCategory>();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
API/DTOs/OPDS/FeedEntryContent.cs
Normal file
12
API/DTOs/OPDS/FeedEntryContent.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public class FeedEntryContent
|
||||||
|
{
|
||||||
|
[XmlAttribute("type")]
|
||||||
|
public string Type = "text";
|
||||||
|
[XmlText]
|
||||||
|
public string Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
API/DTOs/OPDS/FeedLink.cs
Normal file
33
API/DTOs/OPDS/FeedLink.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public class FeedLink
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Relation on the Link
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute("rel")]
|
||||||
|
public string Rel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should be any of the types here <see cref="FeedLinkType"/>
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("href")]
|
||||||
|
public string Href { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||||
|
public int TotalPages { get; set; } = 0;
|
||||||
|
|
||||||
|
public bool ShouldSerializeTotalPages()
|
||||||
|
{
|
||||||
|
return TotalPages > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
API/DTOs/OPDS/FeedLinkRelation.cs
Normal file
24
API/DTOs/OPDS/FeedLinkRelation.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public static class FeedLinkRelation
|
||||||
|
{
|
||||||
|
public const string Debug = "debug";
|
||||||
|
public const string Search = "search";
|
||||||
|
public const string Self = "self";
|
||||||
|
public const string Start = "start";
|
||||||
|
public const string Next = "next";
|
||||||
|
public const string Prev = "prev";
|
||||||
|
public const string Alternate = "alternate";
|
||||||
|
public const string SubSection = "subsection";
|
||||||
|
public const string Related = "related";
|
||||||
|
public const string Image = "http://opds-spec.org/image";
|
||||||
|
public const string Thumbnail = "http://opds-spec.org/image/thumbnail";
|
||||||
|
/// <summary>
|
||||||
|
/// This will allow for a download to occur
|
||||||
|
/// </summary>
|
||||||
|
public const string Acquisition = "http://opds-spec.org/acquisition/open-access";
|
||||||
|
#pragma warning disable S1075
|
||||||
|
public const string Stream = "http://vaemendis.net/opds-pse/stream";
|
||||||
|
#pragma warning restore S1075
|
||||||
|
}
|
||||||
|
}
|
||||||
11
API/DTOs/OPDS/FeedLinkType.cs
Normal file
11
API/DTOs/OPDS/FeedLinkType.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public static class FeedLinkType
|
||||||
|
{
|
||||||
|
public const string Atom = "application/atom+xml";
|
||||||
|
public const string AtomSearch = "application/opensearchdescription+xml";
|
||||||
|
public const string AtomNavigation = "application/atom+xml;profile=opds-catalog;kind=navigation";
|
||||||
|
public const string AtomAcquisition = "application/atom+xml;profile=opds-catalog;kind=acquisition";
|
||||||
|
public const string Image = "image/jpeg";
|
||||||
|
}
|
||||||
|
}
|
||||||
42
API/DTOs/OPDS/OpenSearchDescription.cs
Normal file
42
API/DTOs/OPDS/OpenSearchDescription.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
[XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
|
||||||
|
public class OpenSearchDescription
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a brief human-readable title that identifies this search engine.
|
||||||
|
/// </summary>
|
||||||
|
public string ShortName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Contains an extended human-readable title that identifies this search engine.
|
||||||
|
/// </summary>
|
||||||
|
public string LongName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a human-readable text description of the search engine.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element
|
||||||
|
/// </summary>
|
||||||
|
public SearchLink Url { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a set of words that are used as keywords to identify and categorize this search content.
|
||||||
|
/// Tags must be a single word and are delimited by the space character (' ').
|
||||||
|
/// </summary>
|
||||||
|
public string Tags { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a URL that identifies the location of an image that can be used in association with this search content.
|
||||||
|
/// <example><Image height="64" width="64" type="image/png">http://example.com/websearch.png</Image></example>
|
||||||
|
/// </summary>
|
||||||
|
public string Image { get; set; }
|
||||||
|
public string InputEncoding { get; set; } = "UTF-8";
|
||||||
|
public string OutputEncoding { get; set; } = "UTF-8";
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the human-readable name or identifier of the creator or maintainer of the description document.
|
||||||
|
/// </summary>
|
||||||
|
public string Developer { get; set; } = "kavitareader.com";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
16
API/DTOs/OPDS/SearchLink.cs
Normal file
16
API/DTOs/OPDS/SearchLink.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace API.DTOs.OPDS
|
||||||
|
{
|
||||||
|
public class SearchLink
|
||||||
|
{
|
||||||
|
[XmlAttribute("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("rel")]
|
||||||
|
public string Rel { get; set; } = "results";
|
||||||
|
|
||||||
|
[XmlAttribute("template")]
|
||||||
|
public string Template { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
API/DTOs/ProgressDto.cs
Normal file
15
API/DTOs/ProgressDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class ProgressDto
|
||||||
|
{
|
||||||
|
public int VolumeId { get; set; }
|
||||||
|
public int ChapterId { get; set; }
|
||||||
|
public int PageNum { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
|
||||||
|
/// on pages that combine multiple "chapters".
|
||||||
|
/// </summary>
|
||||||
|
public string BookScrollId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
API/DTOs/RemoveBookmarkForSeriesDto.cs
Normal file
7
API/DTOs/RemoveBookmarkForSeriesDto.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class RemoveBookmarkForSeriesDto
|
||||||
|
{
|
||||||
|
public int SeriesId { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
API/DTOs/SeriesByIdsDto.cs
Normal file
7
API/DTOs/SeriesByIdsDto.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class SeriesByIdsDto
|
||||||
|
{
|
||||||
|
public int[] SeriesIds { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ namespace API.DTOs
|
||||||
public string SortName { get; init; }
|
public string SortName { get; init; }
|
||||||
public string Summary { get; init; }
|
public string Summary { get; init; }
|
||||||
public int Pages { get; init; }
|
public int Pages { get; init; }
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sum of pages read from linked Volumes. Calculated at API-time.
|
/// Sum of pages read from linked Volumes. Calculated at API-time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,22 @@
|
||||||
{
|
{
|
||||||
public string CacheDirectory { get; set; }
|
public string CacheDirectory { get; set; }
|
||||||
public string TaskScan { get; set; }
|
public string TaskScan { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Logging level for server. Managed in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
public string LoggingLevel { get; set; }
|
public string LoggingLevel { get; set; }
|
||||||
public string TaskBackup { get; set; }
|
public string TaskBackup { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Port the server listens on. Managed in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Allows anonymous information to be collected and sent to KavitaStats
|
||||||
|
/// </summary>
|
||||||
public bool AllowStatCollection { get; set; }
|
public bool AllowStatCollection { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Enables OPDS connections to be made to the server.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableOpds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal file
38
API/DTOs/Update/UpdateNotificationDto.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
namespace API.DTOs.Update
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update Notification denoting a new release available for user to update to
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateNotificationDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current installed Version
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentVersion { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Semver of the release version
|
||||||
|
/// <example>0.4.3</example>
|
||||||
|
/// </summary>
|
||||||
|
public string UpdateVersion { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Release body in HTML
|
||||||
|
/// </summary>
|
||||||
|
public string UpdateBody { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Title of the release
|
||||||
|
/// </summary>
|
||||||
|
public string UpdateTitle { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Github Url
|
||||||
|
/// </summary>
|
||||||
|
public string UpdateUrl { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// If this install is within Docker
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDocker { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is this a pre-release
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrerelease { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,5 +10,6 @@
|
||||||
public byte[] CoverImage { get; init; }
|
public byte[] CoverImage { get; init; }
|
||||||
public int UserRating { get; set; }
|
public int UserRating { get; set; }
|
||||||
public string UserReview { get; set; }
|
public string UserReview { get; set; }
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
API/DTOs/Uploads/UploadFileDto.cs
Normal file
14
API/DTOs/Uploads/UploadFileDto.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace API.DTOs.Uploads
|
||||||
|
{
|
||||||
|
public class UploadFileDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of the Entity
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Url of the file to download from (can be null)
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace API.DTOs
|
||||||
{
|
{
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
public string Token { get; init; }
|
public string Token { get; init; }
|
||||||
|
public string ApiKey { get; init; }
|
||||||
public UserPreferencesDto Preferences { get; set; }
|
public UserPreferencesDto Preferences { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
API/Data/BookmarkRepository.cs
Normal file
7
API/Data/BookmarkRepository.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace API.Data
|
||||||
|
{
|
||||||
|
public class BookmarkRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
23
API/Data/ChapterRepository.cs
Normal file
23
API/Data/ChapterRepository.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data
|
||||||
|
{
|
||||||
|
public class ChapterRepository : IChapterRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
|
||||||
|
public ChapterRepository(DataContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(Chapter chapter)
|
||||||
|
{
|
||||||
|
_context.Entry(chapter).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move over Chapter based queries here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,12 +25,34 @@ namespace API.Data
|
||||||
{
|
{
|
||||||
_context.CollectionTag.Remove(tag);
|
_context.CollectionTag.Remove(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(CollectionTag tag)
|
public void Update(CollectionTag tag)
|
||||||
{
|
{
|
||||||
_context.Entry(tag).State = EntityState.Modified;
|
_context.Entry(tag).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes any collection tags without any series
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> RemoveTagsWithoutSeries()
|
||||||
|
{
|
||||||
|
var tagsToDelete = await _context.CollectionTag
|
||||||
|
.Include(c => c.SeriesMetadatas)
|
||||||
|
.Where(c => c.SeriesMetadatas.Count == 0)
|
||||||
|
.ToListAsync();
|
||||||
|
_context.RemoveRange(tagsToDelete);
|
||||||
|
|
||||||
|
return await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync()
|
||||||
|
{
|
||||||
|
return await _context.CollectionTag
|
||||||
|
.Select(c => c)
|
||||||
|
.OrderBy(c => c.NormalizedTitle)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
|
|
@ -40,7 +62,7 @@ namespace API.Data
|
||||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
|
|
@ -57,7 +79,7 @@ namespace API.Data
|
||||||
.Where(c => c.Id == tagId)
|
.Where(c => c.Id == tagId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CollectionTag> GetFullTagAsync(int tagId)
|
public async Task<CollectionTag> GetFullTagAsync(int tagId)
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
|
|
@ -69,7 +91,7 @@ namespace API.Data
|
||||||
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
|
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
||||||
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
||||||
.OrderBy(s => s.Title)
|
.OrderBy(s => s.Title)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|
@ -87,4 +109,4 @@ namespace API.Data
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
@ -8,7 +11,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
|
||||||
namespace API.Data
|
namespace API.Data
|
||||||
{
|
{
|
||||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||||
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
||||||
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
||||||
{
|
{
|
||||||
|
|
@ -17,10 +20,10 @@ namespace API.Data
|
||||||
ChangeTracker.Tracked += OnEntityTracked;
|
ChangeTracker.Tracked += OnEntityTracked;
|
||||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Library> Library { get; set; }
|
public DbSet<Library> Library { get; set; }
|
||||||
public DbSet<Series> Series { get; set; }
|
public DbSet<Series> Series { get; set; }
|
||||||
|
|
||||||
public DbSet<Chapter> Chapter { get; set; }
|
public DbSet<Chapter> Chapter { get; set; }
|
||||||
public DbSet<Volume> Volume { get; set; }
|
public DbSet<Volume> Volume { get; set; }
|
||||||
public DbSet<AppUser> AppUser { get; set; }
|
public DbSet<AppUser> AppUser { get; set; }
|
||||||
|
|
@ -31,18 +34,19 @@ namespace API.Data
|
||||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
||||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||||
|
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
|
||||||
builder.Entity<AppUser>()
|
builder.Entity<AppUser>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.User)
|
.WithOne(u => u.User)
|
||||||
.HasForeignKey(ur => ur.UserId)
|
.HasForeignKey(ur => ur.UserId)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Entity<AppRole>()
|
builder.Entity<AppRole>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.Role)
|
.WithOne(u => u.Role)
|
||||||
|
|
@ -50,7 +54,7 @@ namespace API.Data
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||||
|
|
@ -58,7 +62,7 @@ namespace API.Data
|
||||||
entity.Created = DateTime.Now;
|
entity.Created = DateTime.Now;
|
||||||
entity.LastModified = DateTime.Now;
|
entity.LastModified = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||||
|
|
@ -66,5 +70,48 @@ namespace API.Data
|
||||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||||
entity.LastModified = DateTime.Now;
|
entity.LastModified = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSaveChanges()
|
||||||
|
{
|
||||||
|
foreach (var saveEntity in ChangeTracker.Entries()
|
||||||
|
.Where(e => e.State == EntityState.Modified)
|
||||||
|
.Select(entry => entry.Entity)
|
||||||
|
.OfType<IHasConcurrencyToken>())
|
||||||
|
{
|
||||||
|
saveEntity.OnSavingChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region SaveChanges overrides
|
||||||
|
|
||||||
|
public override int SaveChanges()
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ namespace API.Data
|
||||||
IsSpecial = specialTreatment,
|
IsSpecial = specialTreatment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
|
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
|
||||||
{
|
{
|
||||||
return new SeriesMetadata()
|
return new SeriesMetadata()
|
||||||
|
|
@ -66,11 +66,11 @@ namespace API.Data
|
||||||
return new CollectionTag()
|
return new CollectionTag()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
NormalizedTitle = API.Parser.Parser.Normalize(title).ToUpper(),
|
NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||||
Title = title,
|
Title = title?.Trim(),
|
||||||
Summary = summary,
|
Summary = summary?.Trim(),
|
||||||
Promoted = promoted
|
Promoted = promoted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace API.Data
|
||||||
{
|
{
|
||||||
var fileExtensions = await _dbContext.MangaFile
|
var fileExtensions = await _dbContext.MangaFile
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Select(x => x.FilePath)
|
.Select(x => x.FilePath.ToLower())
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
|
@ -32,4 +32,4 @@ namespace API.Data
|
||||||
return uniqueFileTypes;
|
return uniqueFileTypes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
913
API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs
generated
Normal file
913
API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,913 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20210809210326_BookmarkPages")]
|
||||||
|
partial class BookmarkPages
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Id", "SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithOne("UserPreferences")
|
||||||
|
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany("Progresses")
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.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.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.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Chapters")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ChapterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
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("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.CollectionTag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CollectionTagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesMetadatasId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserPreferences");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Chapters");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
API/Data/Migrations/20210809210326_BookmarkPages.cs
Normal file
44
API/Data/Migrations/20210809210326_BookmarkPages.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class BookmarkPages : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AppUserBookmark",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Page = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
VolumeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AppUserBookmark", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AppUserBookmark_AspNetUsers_AppUserId",
|
||||||
|
column: x => x.AppUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AppUserBookmark_AppUserId",
|
||||||
|
table: "AppUserBookmark",
|
||||||
|
column: "AppUserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AppUserBookmark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
919
API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs
generated
Normal file
919
API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,919 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20210813010210_CoverImageLockFieldsPart1")]
|
||||||
|
partial class CoverImageLockFieldsPart1
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Id", "SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithOne("UserPreferences")
|
||||||
|
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany("Progresses")
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.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.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.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Chapters")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ChapterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
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("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.CollectionTag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CollectionTagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesMetadatasId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserPreferences");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Chapters");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class CoverImageLockFieldsPart1 : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "CollectionTag",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "CollectionTag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
922
API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs
generated
Normal file
922
API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,922 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20210814215831_CoverImageLockedFieldsPart2")]
|
||||||
|
partial class CoverImageLockedFieldsPart2
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Id", "SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithOne("UserPreferences")
|
||||||
|
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany("Progresses")
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.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.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.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Chapters")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ChapterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
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("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.CollectionTag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CollectionTagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesMetadatasId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserPreferences");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Chapters");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class CoverImageLockedFieldsPart2 : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CoverImageLocked",
|
||||||
|
table: "Chapter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
926
API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs
generated
Normal file
926
API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,926 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20210817152226_ProgressConcurencyCheck")]
|
||||||
|
partial class ProgressConcurencyCheck
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Id", "SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithOne("UserPreferences")
|
||||||
|
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany("Progresses")
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.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.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.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Chapters")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ChapterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
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("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.CollectionTag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CollectionTagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesMetadatasId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserPreferences");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Chapters");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ProgressConcurencyCheck : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<uint>(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "AppUserProgresses",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "AppUserProgresses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
928
API/Data/Migrations/20210826203258_userApiKey.Designer.cs
generated
Normal file
928
API/Data/Migrations/20210826203258_userApiKey.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,928 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20210826203258_userApiKey")]
|
||||||
|
partial class userApiKey
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ApiKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Id", "SeriesId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithOne("UserPreferences")
|
||||||
|
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany("Progresses")
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.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.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.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Chapters")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ChapterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.SeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithOne("Metadata")
|
||||||
|
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
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("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.CollectionTag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CollectionTagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesMetadatasId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserPreferences");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Chapters");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
API/Data/Migrations/20210826203258_userApiKey.cs
Normal file
23
API/Data/Migrations/20210826203258_userApiKey.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class userApiKey : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ApiKey",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ApiKey",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ namespace API.Data.Migrations
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.4");
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
|
|
@ -52,6 +52,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<int>("AccessFailedCount")
|
b.Property<int>("AccessFailedCount")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ApiKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
b.Property<string>("ConcurrencyStamp")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
@ -118,6 +121,34 @@ namespace API.Data.Migrations
|
||||||
b.ToTable("AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
@ -198,6 +229,10 @@ namespace API.Data.Migrations
|
||||||
b.Property<int>("PagesRead")
|
b.Property<int>("PagesRead")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|
@ -260,6 +295,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<byte[]>("CoverImage")
|
b.Property<byte[]>("CoverImage")
|
||||||
.HasColumnType("BLOB");
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime>("Created")
|
b.Property<DateTime>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|
@ -300,6 +338,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<byte[]>("CoverImage")
|
b.Property<byte[]>("CoverImage")
|
||||||
.HasColumnType("BLOB");
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("NormalizedTitle")
|
b.Property<string>("NormalizedTitle")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|
@ -307,7 +348,6 @@ namespace API.Data.Migrations
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<uint>("RowVersion")
|
b.Property<uint>("RowVersion")
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
b.Property<string>("Summary")
|
||||||
|
|
@ -409,6 +449,9 @@ namespace API.Data.Migrations
|
||||||
b.Property<byte[]>("CoverImage")
|
b.Property<byte[]>("CoverImage")
|
||||||
.HasColumnType("BLOB");
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime>("Created")
|
b.Property<DateTime>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|
@ -641,6 +684,17 @@ namespace API.Data.Migrations
|
||||||
b.ToTable("AspNetUserTokens");
|
b.ToTable("AspNetUserTokens");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.AppUserPreferences", b =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
|
@ -832,6 +886,8 @@ namespace API.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUser", b =>
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
b.Navigation("Progresses");
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
b.Navigation("Ratings");
|
b.Navigation("Ratings");
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ namespace API.Data
|
||||||
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))},
|
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))},
|
||||||
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
|
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
|
||||||
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||||
|
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var defaultSetting in defaultSettings)
|
foreach (var defaultSetting in defaultSettings)
|
||||||
|
|
@ -60,30 +61,29 @@ namespace API.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
||||||
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.Port).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
||||||
Configuration.Port + string.Empty;
|
Configuration.Port + string.Empty;
|
||||||
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
||||||
Configuration.LogLevel + string.Empty;
|
Configuration.LogLevel + string.Empty;
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task SeedSeriesMetadata(DataContext context)
|
public static async Task SeedUserApiKeys(DataContext context)
|
||||||
{
|
{
|
||||||
await context.Database.EnsureCreatedAsync();
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
context.Database.EnsureCreated();
|
|
||||||
var series = await context.Series
|
|
||||||
.Include(s => s.Metadata).ToListAsync();
|
|
||||||
|
|
||||||
foreach (var s in series)
|
|
||||||
{
|
|
||||||
s.Metadata ??= new SeriesMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var users = await context.AppUser.ToListAsync();
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(user.ApiKey))
|
||||||
|
{
|
||||||
|
user.ApiKey = HashUtil.ApiKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
|
@ -75,10 +76,11 @@ namespace API.Data
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
|
var formats = filter.GetSqlFilter();
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId && formats.Contains(s.Format))
|
||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
|
|
@ -120,7 +122,7 @@ namespace API.Data
|
||||||
|
|
||||||
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||||
{
|
{
|
||||||
foreach (var v in volumes.Where(vdto => vdto.Number == 0))
|
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||||
{
|
{
|
||||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
||||||
}
|
}
|
||||||
|
|
@ -302,9 +304,12 @@ namespace API.Data
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||||
/// <param name="userParams">Contains pagination information</param>
|
/// <param name="userParams">Contains pagination information</param>
|
||||||
|
/// <param name="filter">Optional filter on query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
|
var formats = filter.GetSqlFilter();
|
||||||
|
|
||||||
if (libraryId == 0)
|
if (libraryId == 0)
|
||||||
{
|
{
|
||||||
var userLibraries = _context.Library
|
var userLibraries = _context.Library
|
||||||
|
|
@ -315,8 +320,7 @@ namespace API.Data
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var allQuery = _context.Series
|
var allQuery = _context.Series
|
||||||
.Where(s => userLibraries.Contains(s.LibraryId))
|
.Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format))
|
||||||
.AsNoTracking()
|
|
||||||
.OrderByDescending(s => s.Created)
|
.OrderByDescending(s => s.Created)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
|
|
@ -325,10 +329,10 @@ namespace API.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId && formats.Contains(s.Format))
|
||||||
.AsNoTracking()
|
|
||||||
.OrderByDescending(s => s.Created)
|
.OrderByDescending(s => s.Created)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
.AsSplitQuery()
|
||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
|
|
||||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
|
@ -338,47 +342,50 @@ namespace API.Data
|
||||||
/// Returns Series that the user has some partial progress on
|
/// Returns Series that the user has some partial progress on
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||||
/// <param name="limit"></param>
|
/// <param name="userParams">Pagination information</param>
|
||||||
|
/// <param name="filter">Optional (default null) filter on query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit)
|
public async Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
var series = _context.Series
|
var formats = filter.GetSqlFilter();
|
||||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
|
IList<int> userLibraries;
|
||||||
{
|
|
||||||
Series = s,
|
|
||||||
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id).Sum(s1 => s1.PagesRead),
|
|
||||||
progress.AppUserId,
|
|
||||||
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id).Max(p => p.LastModified)
|
|
||||||
});
|
|
||||||
if (libraryId == 0)
|
if (libraryId == 0)
|
||||||
{
|
{
|
||||||
var userLibraries = _context.Library
|
userLibraries = _context.Library
|
||||||
.Include(l => l.AppUsers)
|
.Include(l => l.AppUsers)
|
||||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Select(library => library.Id)
|
.Select(library => library.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
series = series.Where(s => s.AppUserId == userId
|
|
||||||
&& s.PagesRead > 0
|
|
||||||
&& s.PagesRead < s.Series.Pages
|
|
||||||
&& userLibraries.Contains(s.Series.LibraryId));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
series = series.Where(s => s.AppUserId == userId
|
userLibraries = new List<int>() {libraryId};
|
||||||
&& s.PagesRead > 0
|
|
||||||
&& s.PagesRead < s.Series.Pages
|
|
||||||
&& s.Series.LibraryId == libraryId);
|
|
||||||
}
|
}
|
||||||
var retSeries = await series
|
|
||||||
.OrderByDescending(s => s.LastModified)
|
|
||||||
.Select(s => s.Series)
|
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return retSeries.DistinctBy(s => s.Name).Take(limit);
|
var series = _context.Series
|
||||||
|
.Where(s => formats.Contains(s.Format) && userLibraries.Contains(s.LibraryId))
|
||||||
|
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
|
||||||
|
{
|
||||||
|
Series = s,
|
||||||
|
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId).Sum(s1 => s1.PagesRead),
|
||||||
|
progress.AppUserId,
|
||||||
|
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified)
|
||||||
|
})
|
||||||
|
.AsNoTracking();
|
||||||
|
|
||||||
|
var retSeries = series.Where(s => s.AppUserId == userId
|
||||||
|
&& s.PagesRead > 0
|
||||||
|
&& s.PagesRead < s.Series.Pages)
|
||||||
|
.OrderByDescending(s => s.LastModified)
|
||||||
|
.Select(s => s.Series)
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.AsNoTracking();
|
||||||
|
|
||||||
|
// Pagination does not work for this query as when we pull the data back, we get multiple rows of the same series. See controller for pagination code
|
||||||
|
return await retSeries.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
|
public async Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
|
||||||
|
|
@ -434,5 +441,21 @@ namespace API.Data
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId)
|
||||||
|
{
|
||||||
|
var allowedLibraries = _context.Library
|
||||||
|
.Include(l => l.AppUsers)
|
||||||
|
.Where(library => library.AppUsers.Any(x => x.Id == userId))
|
||||||
|
.Select(l => l.Id);
|
||||||
|
|
||||||
|
return await _context.Series
|
||||||
|
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||||
|
.OrderBy(s => s.SortName)
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Repositories;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ namespace API.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
|
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
|
||||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
|
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
|
||||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||||
|
|
||||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
||||||
|
|
@ -30,30 +31,52 @@ namespace API.Data
|
||||||
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context);
|
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context);
|
||||||
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
|
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
|
||||||
public IFileRepository FileRepository => new FileRepository(_context);
|
public IFileRepository FileRepository => new FileRepository(_context);
|
||||||
|
public IChapterRepository ChapterRepository => new ChapterRepository(_context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool Commit()
|
public bool Commit()
|
||||||
{
|
{
|
||||||
|
|
||||||
return _context.SaveChanges() > 0;
|
return _context.SaveChanges() > 0;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<bool> CommitAsync()
|
public async Task<bool> CommitAsync()
|
||||||
{
|
{
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the DB Context aware of Changes in loaded entities
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool HasChanges()
|
public bool HasChanges()
|
||||||
{
|
{
|
||||||
return _context.ChangeTracker.HasChanges();
|
return _context.ChangeTracker.HasChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rollback transaction
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<bool> RollbackAsync()
|
public async Task<bool> RollbackAsync()
|
||||||
{
|
{
|
||||||
await _context.DisposeAsync();
|
await _context.DisposeAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Rollback transaction
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool Rollback()
|
public bool Rollback()
|
||||||
{
|
{
|
||||||
_context.Dispose();
|
_context.Dispose();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using API.Constants;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
@ -14,18 +16,20 @@ namespace API.Data
|
||||||
{
|
{
|
||||||
private readonly DataContext _context;
|
private readonly DataContext _context;
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
public UserRepository(DataContext context, UserManager<AppUser> userManager)
|
public UserRepository(DataContext context, UserManager<AppUser> userManager, IMapper mapper)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(AppUser user)
|
public void Update(AppUser user)
|
||||||
{
|
{
|
||||||
_context.Entry(user).State = EntityState.Modified;
|
_context.Entry(user).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(AppUserPreferences preferences)
|
public void Update(AppUserPreferences preferences)
|
||||||
{
|
{
|
||||||
_context.Entry(preferences).State = EntityState.Modified;
|
_context.Entry(preferences).State = EntityState.Modified;
|
||||||
|
|
@ -45,9 +49,23 @@ namespace API.Data
|
||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
.Include(u => u.Progresses)
|
.Include(u => u.Progresses)
|
||||||
|
.Include(u => u.Bookmarks)
|
||||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an AppUser by id. Returns back Progress information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<AppUser> GetUserByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.Users
|
||||||
|
.Include(u => u.Progresses)
|
||||||
|
.Include(u => u.Bookmarks)
|
||||||
|
.SingleOrDefaultAsync(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
|
|
@ -71,6 +89,53 @@ namespace API.Data
|
||||||
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserBookmark
|
||||||
|
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
|
||||||
|
.OrderBy(x => x.Page)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserBookmark
|
||||||
|
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
|
||||||
|
.OrderBy(x => x.Page)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserBookmark
|
||||||
|
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
|
||||||
|
.OrderBy(x => x.Page)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserBookmark
|
||||||
|
.Where(x => x.AppUserId == userId)
|
||||||
|
.OrderBy(x => x.Page)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppUser> GetUserByApiKeyAsync(string apiKey)
|
||||||
|
{
|
||||||
|
return await _context.AppUser
|
||||||
|
.SingleOrDefaultAsync(u => u.ApiKey.Equals(apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
|
|
@ -97,4 +162,4 @@ namespace API.Data
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ namespace API.Data
|
||||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Chapters for a volume id.
|
/// Returns Chapters for a volume id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -79,13 +78,30 @@ namespace API.Data
|
||||||
return chapter;
|
return chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<MangaFile>> GetFilesForChapter(int chapterId)
|
/// <summary>
|
||||||
|
/// Returns non-tracked files for a given chapterId
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
|
||||||
{
|
{
|
||||||
return await _context.MangaFile
|
return await _context.MangaFile
|
||||||
.Where(c => chapterId == c.ChapterId)
|
.Where(c => chapterId == c.ChapterId)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns non-tracked files for a set of chapterIds
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterIds"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
|
||||||
|
{
|
||||||
|
return await _context.MangaFile
|
||||||
|
.Where(c => chapterIds.Contains(c.ChapterId))
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,22 @@ namespace API.Entities
|
||||||
public ICollection<AppUserProgress> Progresses { get; set; }
|
public ICollection<AppUserProgress> Progresses { get; set; }
|
||||||
public ICollection<AppUserRating> Ratings { get; set; }
|
public ICollection<AppUserRating> Ratings { get; set; }
|
||||||
public AppUserPreferences UserPreferences { get; set; }
|
public AppUserPreferences UserPreferences { get; set; }
|
||||||
|
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
||||||
[ConcurrencyCheck]
|
/// <summary>
|
||||||
public uint RowVersion { get; set; }
|
/// An API Key to interact with external services, like OPDS
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[ConcurrencyCheck]
|
||||||
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
API/Entities/AppUserBookmark.cs
Normal file
22
API/Entities/AppUserBookmark.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace API.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a saved page in a Chapter entity for a given user.
|
||||||
|
/// </summary>
|
||||||
|
public class AppUserBookmark
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public int VolumeId { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public int ChapterId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
[JsonIgnore]
|
||||||
|
public AppUser AppUser { get; set; }
|
||||||
|
public int AppUserId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
|
|
@ -7,24 +8,63 @@ namespace API.Entities
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the progress a single user has on a given Chapter.
|
/// Represents the progress a single user has on a given Chapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppUserProgress : IEntityDate
|
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), nameof(AppUserId), IsUnique = true)]
|
||||||
|
public class AppUserProgress : IEntityDate, IHasConcurrencyToken
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of Entity
|
||||||
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Pages Read for given Chapter
|
||||||
|
/// </summary>
|
||||||
public int PagesRead { get; set; }
|
public int PagesRead { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Volume belonging to Chapter
|
||||||
|
/// </summary>
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Series belonging to Chapter
|
||||||
|
/// </summary>
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Chapter
|
||||||
|
/// </summary>
|
||||||
public int ChapterId { get; set; }
|
public int ChapterId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
|
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
|
||||||
/// on next load
|
/// on next load
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BookScrollId { get; set; }
|
public string BookScrollId { get; set; }
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
|
/// <summary>
|
||||||
|
/// Navigational Property for EF. Links to a unique AppUser
|
||||||
|
/// </summary>
|
||||||
public AppUser AppUser { get; set; }
|
public AppUser AppUser { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// User this progress belongs to
|
||||||
|
/// </summary>
|
||||||
public int AppUserId { get; set; }
|
public int AppUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this was first created
|
||||||
|
/// </summary>
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Last date this was updated
|
||||||
|
/// </summary>
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[ConcurrencyCheck]
|
||||||
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnSavingChanges()
|
||||||
|
{
|
||||||
|
RowVersion++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace API.Entities
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total number of pages in all MangaFiles
|
/// Total number of pages in all MangaFiles
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -33,7 +34,7 @@ namespace API.Entities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSpecial { get; set; }
|
public bool IsSpecial { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
|
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ namespace API.Entities
|
||||||
Title = (IsSpecial && info.Format == MangaFormat.Epub)
|
Title = (IsSpecial && info.Format == MangaFormat.Epub)
|
||||||
? info.Title
|
? info.Title
|
||||||
: Range;
|
: Range;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using API.Entities.Interfaces;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
|
|
@ -9,19 +7,23 @@ namespace API.Entities
|
||||||
/// Represents a user entered field that is used as a tagging and grouping mechanism
|
/// Represents a user entered field that is used as a tagging and grouping mechanism
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Index(nameof(Id), nameof(Promoted), IsUnique = true)]
|
[Index(nameof(Id), nameof(Promoted), IsUnique = true)]
|
||||||
public class CollectionTag : IHasConcurrencyToken
|
public class CollectionTag
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visible title of the Tag
|
/// Visible title of the Tag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cover Image for the collection tag
|
/// Cover Image for the collection tag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations.
|
||||||
|
/// </summary>
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A description of the tag
|
/// A description of the tag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -35,16 +37,20 @@ namespace API.Entities
|
||||||
/// A promoted collection tag will allow all linked seriesMetadata's Series to show for all users.
|
/// A promoted collection tag will allow all linked seriesMetadata's Series to show for all users.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Promoted { get; set; }
|
public bool Promoted { get; set; }
|
||||||
|
|
||||||
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[ConcurrencyCheck]
|
/// <summary>
|
||||||
public uint RowVersion { get; set; }
|
/// Not Used due to not using concurrency update
|
||||||
|
/// </summary>
|
||||||
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not Used due to not using concurrency update
|
||||||
|
/// </summary>
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,36 @@
|
||||||
|
|
||||||
namespace API.Entities.Enums
|
namespace API.Entities.Enums
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the format of the file
|
||||||
|
/// </summary>
|
||||||
public enum MangaFormat
|
public enum MangaFormat
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Image file
|
||||||
|
/// See <see cref="Parser.Parser.ImageFileExtensions"/> for supported extensions
|
||||||
|
/// </summary>
|
||||||
[Description("Image")]
|
[Description("Image")]
|
||||||
Image = 0,
|
Image = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// Archive based file
|
||||||
|
/// See <see cref="Parser.Parser.ArchiveFileExtensions"/> for supported extensions
|
||||||
|
/// </summary>
|
||||||
[Description("Archive")]
|
[Description("Archive")]
|
||||||
Archive = 1,
|
Archive = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown. Not used.
|
||||||
|
/// </summary>
|
||||||
[Description("Unknown")]
|
[Description("Unknown")]
|
||||||
Unknown = 2,
|
Unknown = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// EPUB File
|
||||||
|
/// </summary>
|
||||||
[Description("EPUB")]
|
[Description("EPUB")]
|
||||||
Epub = 3,
|
Epub = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// PDF File
|
||||||
|
/// </summary>
|
||||||
[Description("PDF")]
|
[Description("PDF")]
|
||||||
Pdf = 4
|
Pdf = 4
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ namespace API.Entities.Enums
|
||||||
BackupDirectory = 5,
|
BackupDirectory = 5,
|
||||||
[Description("AllowStatCollection")]
|
[Description("AllowStatCollection")]
|
||||||
AllowStatCollection = 6,
|
AllowStatCollection = 6,
|
||||||
|
[Description("EnableOpds")]
|
||||||
|
EnableOpds = 7,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ namespace API.Entities
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
// MetadataUpdate add ProviderId
|
// MetadataUpdate add ProviderId
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a wrapper to the underlying file. This provides information around file, like number of pages, format, etc.
|
||||||
|
/// </summary>
|
||||||
public class MangaFile
|
public class MangaFile
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ namespace API.Entities
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public PersonRole Role { get; set; }
|
public PersonRole Role { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ namespace API.Entities
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations.
|
||||||
|
/// </summary>
|
||||||
|
public bool CoverImageLocked { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Sum of all Volume page counts
|
/// Sum of all Volume page counts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ namespace API.Entities
|
||||||
public Series Series { get; set; }
|
public Series Series { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ namespace API.Entities
|
||||||
public ServerSettingKey Key { get; set; }
|
public ServerSettingKey Key { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using API.SignalR.Presence;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -32,9 +33,15 @@ namespace API.Extensions
|
||||||
services.AddScoped<ICleanupService, CleanupService>();
|
services.AddScoped<ICleanupService, CleanupService>();
|
||||||
services.AddScoped<IBookService, BookService>();
|
services.AddScoped<IBookService, BookService>();
|
||||||
services.AddScoped<IImageService, ImageService>();
|
services.AddScoped<IImageService, ImageService>();
|
||||||
|
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
|
||||||
|
services.AddScoped<IDownloadService, DownloadService>();
|
||||||
|
services.AddScoped<IReaderService, ReaderService>();
|
||||||
|
|
||||||
|
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||||
|
|
||||||
services.AddSqLite(config, env);
|
services.AddSqLite(config, env);
|
||||||
services.AddLogging(config);
|
services.AddLogging(config);
|
||||||
|
services.AddSignalR();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@ namespace API.Extensions
|
||||||
{
|
{
|
||||||
public static class FileInfoExtensions
|
public static class FileInfoExtensions
|
||||||
{
|
{
|
||||||
public static bool DoesLastWriteMatch(this FileInfo fileInfo, DateTime comparison)
|
/// <summary>
|
||||||
|
/// Checks if the last write time of the file is after the passed date
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileInfo"></param>
|
||||||
|
/// <param name="comparison"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
|
||||||
{
|
{
|
||||||
return comparison.Equals(fileInfo.LastWriteTime);
|
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
|
||||||
}
|
//return fileInfo?.LastWriteTime > comparison;
|
||||||
|
|
||||||
public static bool IsLastWriteLessThan(this FileInfo fileInfo, DateTime comparison)
|
|
||||||
{
|
|
||||||
return fileInfo.LastWriteTime < comparison;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
API/Extensions/FilterDtoExtensions.cs
Normal file
25
API/Extensions/FilterDtoExtensions.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
|
{
|
||||||
|
public static class FilterDtoExtensions
|
||||||
|
{
|
||||||
|
private static readonly IList<MangaFormat> AllFormats = Enum.GetValues<MangaFormat>();
|
||||||
|
|
||||||
|
public static IList<MangaFormat> GetSqlFilter(this FilterDto filter)
|
||||||
|
{
|
||||||
|
var format = filter.MangaFormat;
|
||||||
|
if (format != null)
|
||||||
|
{
|
||||||
|
return new List<MangaFormat>()
|
||||||
|
{
|
||||||
|
(MangaFormat) format
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return AllFormats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
|
@ -24,7 +25,7 @@ namespace API.Extensions
|
||||||
.AddSignInManager<SignInManager<AppUser>>()
|
.AddSignInManager<SignInManager<AppUser>>()
|
||||||
.AddRoleValidator<RoleValidator<AppRole>>()
|
.AddRoleValidator<RoleValidator<AppRole>>()
|
||||||
.AddEntityFrameworkStores<DataContext>();
|
.AddEntityFrameworkStores<DataContext>();
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
|
|
@ -35,14 +36,30 @@ namespace API.Extensions
|
||||||
ValidateIssuer = false,
|
ValidateIssuer = false,
|
||||||
ValidateAudience = false
|
ValidateAudience = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
options.Events = new JwtBearerEvents()
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"];
|
||||||
|
var path = context.HttpContext.Request.Path;
|
||||||
|
// Only use query string based token on SignalR hubs
|
||||||
|
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||||
|
{
|
||||||
|
context.Token = accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
services.AddAuthorization(opt =>
|
services.AddAuthorization(opt =>
|
||||||
{
|
{
|
||||||
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
||||||
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ namespace API.Helpers
|
||||||
CreateMap<Volume, VolumeDto>();
|
CreateMap<Volume, VolumeDto>();
|
||||||
|
|
||||||
CreateMap<MangaFile, MangaFileDto>();
|
CreateMap<MangaFile, MangaFileDto>();
|
||||||
|
|
||||||
CreateMap<Chapter, ChapterDto>();
|
CreateMap<Chapter, ChapterDto>();
|
||||||
|
|
||||||
CreateMap<Series, SeriesDto>();
|
CreateMap<Series, SeriesDto>();
|
||||||
|
|
||||||
CreateMap<CollectionTag, CollectionTagDto>();
|
CreateMap<CollectionTag, CollectionTagDto>();
|
||||||
|
|
||||||
CreateMap<SeriesMetadata, SeriesMetadataDto>();
|
CreateMap<SeriesMetadata, SeriesMetadataDto>();
|
||||||
|
|
@ -29,18 +29,20 @@ namespace API.Helpers
|
||||||
|
|
||||||
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
||||||
|
|
||||||
|
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||||
|
|
||||||
CreateMap<Series, SearchResultDto>()
|
CreateMap<Series, SearchResultDto>()
|
||||||
.ForMember(dest => dest.SeriesId,
|
.ForMember(dest => dest.SeriesId,
|
||||||
opt => opt.MapFrom(src => src.Id))
|
opt => opt.MapFrom(src => src.Id))
|
||||||
.ForMember(dest => dest.LibraryName,
|
.ForMember(dest => dest.LibraryName,
|
||||||
opt => opt.MapFrom(src => src.Library.Name));
|
opt => opt.MapFrom(src => src.Library.Name));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<Library, LibraryDto>()
|
CreateMap<Library, LibraryDto>()
|
||||||
.ForMember(dest => dest.Folders,
|
.ForMember(dest => dest.Folders,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
||||||
|
|
||||||
CreateMap<AppUser, MemberDto>()
|
CreateMap<AppUser, MemberDto>()
|
||||||
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
||||||
|
|
||||||
|
|
@ -50,4 +52,4 @@ namespace API.Helpers
|
||||||
.ConvertUsing<ServerSettingConverter>();
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,13 @@ namespace API.Helpers.Converters
|
||||||
case ServerSettingKey.AllowStatCollection:
|
case ServerSettingKey.AllowStatCollection:
|
||||||
destination.AllowStatCollection = bool.Parse(row.Value);
|
destination.AllowStatCollection = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.EnableOpds:
|
||||||
|
destination.EnableOpds = bool.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,7 @@ namespace API.Interfaces
|
||||||
Task<CollectionTag> GetTagAsync(int tagId);
|
Task<CollectionTag> GetTagAsync(int tagId);
|
||||||
Task<CollectionTag> GetFullTagAsync(int tagId);
|
Task<CollectionTag> GetFullTagAsync(int tagId);
|
||||||
void Update(CollectionTag tag);
|
void Update(CollectionTag tag);
|
||||||
|
Task<int> RemoveTagsWithoutSeries();
|
||||||
|
Task<IEnumerable<CollectionTag>> GetAllTagsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ namespace API.Interfaces
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="userParams"></param>
|
/// <param name="userParams"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Does not add user information like progress, ratings, etc.
|
/// Does not add user information like progress, ratings, etc.
|
||||||
|
|
@ -57,10 +58,11 @@ namespace API.Interfaces
|
||||||
|
|
||||||
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
|
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
|
||||||
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
|
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
|
||||||
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit);
|
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
||||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||||
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||||
|
Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue