diff --git a/.editorconfig b/.editorconfig
index 0979bd12c..bd7557884 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -272,9 +272,9 @@ csharp_new_line_between_query_expression_clauses = true
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
-csharp_indent_case_contents_when_block = true
+csharp_indent_case_contents_when_block = false
csharp_indent_switch_labels = true
-csharp_indent_labels = flush_left
+csharp_indent_labels = no_change
# Whitespace options
csharp_style_allow_embedded_statements_on_same_line_experimental = false
diff --git a/.github/workflows/ReplaceTmdbApiKey.ps1 b/.github/workflows/ReplaceTmdbApiKey.ps1
new file mode 100644
index 000000000..fda2d85bc
--- /dev/null
+++ b/.github/workflows/ReplaceTmdbApiKey.ps1
@@ -0,0 +1,10 @@
+Param(
+ [string] $apiKey = "TMDB_API_KEY_GOES_HERE"
+)
+
+$filename = "./Shoko.Server/Server/Constants.cs"
+$searchString = "TMDB_API_KEY_GOES_HERE"
+
+(Get-Content $filename) | ForEach-Object {
+ $_ -replace $searchString, $apiKey
+} | Set-Content $filename
diff --git a/.github/workflows/build-daily.yml b/.github/workflows/build-daily.yml
index 2352c4149..6a11843bb 100644
--- a/.github/workflows/build-daily.yml
+++ b/.github/workflows/build-daily.yml
@@ -5,6 +5,9 @@ on:
branches:
- master
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+
jobs:
current_info:
runs-on: ubuntu-latest
@@ -12,32 +15,49 @@ jobs:
name: Current Information
outputs:
+ tag: ${{ steps.release_info.outputs.tag }}
version: ${{ steps.release_info.outputs.version }}
date: ${{ steps.commit_date_iso8601.outputs.date }}
sha: ${{ github.sha }}
sha_short: ${{ steps.commit_info.outputs.sha }}
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.sha }}"
submodules: recursive
fetch-depth: 0 # This is set to download the full git history for the repo
+ - name: Get Commit Date (as ISO8601)
+ id: commit_date_iso8601
+ shell: bash
+ env:
+ TZ: UTC0
+ run: |
+ echo "date=$(git --no-pager show -s --date='format-local:%Y-%m-%dT%H:%M:%SZ' --format=%cd ${{ github.sha }})" >> "$GITHUB_OUTPUT"
+
+ - name: Get Previous Version
+ id: previous_release_info
+ uses: revam/gh-action-get-tag-and-version@v1
+ with:
+ branch: false
+ prefix: "v"
+ prefixRegex: "[vV]?"
+ suffixRegex: "dev"
+ suffix: "dev"
+
- name: Get Current Version
id: release_info
uses: revam/gh-action-get-tag-and-version@v1
with:
- branch: true
- prefix: v
+ branch: false
+ increment: "suffix"
+ prefix: "v"
prefixRegex: "[vV]?"
+ suffixRegex: "dev"
+ suffix: "dev"
- - name: Get Commit Date (as ISO8601)
- id: commit_date_iso8601
- shell: bash
- run: |
- echo "date=$(git --no-pager show -s --format=%aI ${{ github.sha }})" >> "$GITHUB_OUTPUT"
- id: commit_info
name: Shorten Commit Hash
uses: actions/github-script@v6
@@ -67,7 +87,7 @@ jobs:
name: Build CLI — ${{ matrix.build_type }} ${{ matrix.rid }} (Daily)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.sha }}"
@@ -77,6 +97,7 @@ jobs:
shell: pwsh
run: |
./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ ./.github/workflows/ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- name: Set up QEMU
@@ -90,12 +111,12 @@ jobs:
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release -r ${{ matrix.rid }} -f net8.0 ${{ matrix.build_props }} Shoko.CLI /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="\"channel=dev,commit=${{ needs.current_info.outputs.sha }},date=${{ needs.current_info.outputs.date }},\""
+ - run: dotnet publish -c Release -r ${{ matrix.rid }} -f net8.0 ${{ matrix.build_props }} Shoko.CLI /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="\"channel=dev,commit=${{ needs.current_info.outputs.sha }},tag=${{ needs.current_info.outputs.tag }},date=${{ needs.current_info.outputs.date }},\""
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: Shoko.CLI_${{ matrix.build_type }}_${{ matrix.rid }}.zip
+ name: Shoko.CLI_${{ matrix.build_type }}_${{ matrix.rid }}
path: Shoko.Server/bin/Release/net8.0/${{matrix.rid}}/publish/
tray-service-daily:
@@ -109,7 +130,7 @@ jobs:
dotnet: [ '8.x' ]
build_type: ['Standalone', 'Framework']
include:
- - build_props: '-r win-x64 --self-contained true -f net8.0-windows'
+ - build_props: '-r win-x64 --self-contained true'
build_type: 'Standalone'
- build_dir: '/net8.0-windows/win-x64'
build_type: 'Standalone'
@@ -121,7 +142,7 @@ jobs:
name: Build Tray Service ${{ matrix.build_type }} (Daily)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.sha }}"
@@ -131,6 +152,7 @@ jobs:
shell: pwsh
run: |
.\\.github\\workflows\\ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ .\\.github\\workflows\\ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
.\\.github\\workflows\\ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- name: Setup dotnet
@@ -138,26 +160,20 @@ jobs:
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release ${{ matrix.build_props }} Shoko.TrayService /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="channel=dev%2ccommit=${{ needs.current_info.outputs.sha }}%2cdate=${{ needs.current_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
+ - run: dotnet publish -c Release ${{ matrix.build_props }} Shoko.TrayService /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="channel=dev%2ccommit=${{ needs.current_info.outputs.sha }}%2ctag=${{ needs.current_info.outputs.tag }}%2cdate=${{ needs.current_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: Shoko.TrayService_${{ matrix.build_type }}_win-x64.zip
+ name: Shoko.TrayService_${{ matrix.build_type }}_win-x64
path: Shoko.Server/bin/Release${{ matrix.build_dir }}/publish/
- - name: Upload to shokoanime.com
- if: ${{ matrix.build_type == 'Standalone' }}
- shell: pwsh
- env:
- FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
- FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
- FTP_SERVER: ${{ secrets.FTP_SERVER }}
- run : Compress-Archive .\\Shoko.Server\\bin\\Release\\net8.0-windows\\win-x64\\publish .\\ShokoServer.zip && .\\.github\\workflows\\UploadArchive.ps1
-
docker-daily-build:
runs-on: ubuntu-latest
+ needs:
+ - current_info
+
strategy:
fail-fast: false
matrix:
@@ -170,11 +186,8 @@ jobs:
name: Build Docker Image - ${{ matrix.arch }} (Daily)
- needs:
- - current_info
-
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.sha }}"
@@ -184,6 +197,7 @@ jobs:
shell: pwsh
run: |
./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ ./.github/workflows/ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- uses: docker/setup-qemu-action@v2
@@ -226,6 +240,7 @@ jobs:
channel=dev
commit=${{ needs.current_info.outputs.sha }}
date=${{ needs.current_info.outputs.date }}
+ tag=${{ needs.current_info.outputs.tag }}
provenance: false
docker-daily-push_manifest:
@@ -260,7 +275,7 @@ jobs:
docker manifest push ghcr.io/${{ secrets.DOCKER_REPO }}:daily
docker manifest push ${{ secrets.DOCKER_REPO }}:daily
- sentry-upload:
+ add-tag:
runs-on: ubuntu-latest
needs:
@@ -269,17 +284,39 @@ jobs:
- tray-service-daily
- docker-daily-push_manifest
+ name: Add tag for pre-release
+
+ steps:
+ - name: Checkout "${{ github.ref }}"
+ uses: actions/checkout@master
+ with:
+ ref: "${{ github.sha }}"
+ submodules: recursive
+
+ - name: Push pre-release tag
+ uses: rickstaa/action-create-tag@v1
+ with:
+ tag: ${{ needs.current_info.outputs.tag }}
+ message: Shoko Server v${{ needs.current_info.outputs.version }} Pre-Release
+
+ sentry-upload:
+ runs-on: ubuntu-latest
+
+ needs:
+ - current_info
+ - add-tag
+
name: Upload version info to Sentry.io
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.sha }}"
submodules: recursive
# Only add the release to sentry if the build is successful.
- - name: Push Sentry Release "${{ needs.current_info.outputs.version }}-dev-${{ needs.current_info.outputs.sha_short }}"
+ - name: Push Sentry Release "${{ needs.current_info.outputs.version }}"
uses: getsentry/action-release@v1.2.1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -288,7 +325,36 @@ jobs:
# SENTRY_URL: https://sentry.io/
with:
environment: 'dev'
- version: ${{ needs.current_info.outputs.version }}-dev-${{ needs.current_info.outputs.sha_short }}
+ version: ${{ needs.current_info.outputs.version }}
+
+ upload-site:
+ runs-on: windows-latest
+ continue-on-error: true
+
+ needs:
+ - sentry-upload
+
+ name: Upload archive to site
+
+ steps:
+ - name: Checkout "${{ github.ref }}"
+ uses: actions/checkout@master
+ with:
+ ref: "${{ github.sha }}"
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: Shoko.TrayService_Standalone_win-x64
+ path: ShokoServer
+
+ - name: Upload daily archive to site
+ shell: pwsh
+ env:
+ FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
+ FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
+ FTP_SERVER: ${{ secrets.FTP_SERVER }}
+ run : Compress-Archive .\\ShokoServer .\\ShokoServer.zip && .\\.github\\workflows\\UploadArchive.ps1
discord-notify:
runs-on: ubuntu-latest
@@ -306,7 +372,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
- RELEASE_VERSION: ${{ needs.current_info.outputs.version }}-dev-${{ needs.current_info.outputs.sha_short }}
+ RELEASE_VERSION: ${{ needs.current_info.outputs.version }}
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "CHANGELOG<<$EOF" >> "$GITHUB_OUTPUT"
@@ -326,7 +392,7 @@ jobs:
embed-author-icon-url: https://raw.githubusercontent.com/${{ github.repository }}/master/.github/images/Shoko.png
embed-author-url: https://github.com/${{ github.repository }}
embed-description: |
- **Version**: `${{ needs.current_info.outputs.version }}-dev-${{ needs.current_info.outputs.sha_short }}`
+ **Version**: `${{ needs.current_info.outputs.version }}` (`${{ needs.current_info.outputs.sha_short }}`)
Update by grabbing the latest daily from [our site](https://shokoanime.com/downloads/shoko-server) or through Docker using the `shokoanime/server:daily` tag!
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 2d7e8821a..b10fcde1f 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -5,32 +5,39 @@ on:
types:
- released
-
jobs:
- cli-framework-stable:
+ current_info:
runs-on: ubuntu-latest
- strategy:
- matrix:
- dotnet: [ '8.x' ]
+ name: Current Information
- name: Build CLI — Framework dependent (Stable)
+ outputs:
+ tag: ${{ steps.release_info.outputs.tag }}
+ tag_major: v${{ steps.release_info.outputs.version_major }}
+ tag_minor: v${{ steps.release_info.outputs.version_major }}.${{ steps.release_info.outputs.version_minor }}
+ version: ${{ steps.release_info.outputs.version }}
+ version_short: ${{ steps.release_info.outputs.version_short }}
+ date: ${{ steps.commit_date_iso8601.outputs.date }}
+ sha: ${{ github.sha }}
+ sha_short: ${{ steps.commit_info.outputs.sha }}
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.sha }}"
uses: actions/checkout@master
with:
- ref: "${{ github.ref }}"
+ ref: "${{ github.sha }}"
submodules: recursive
fetch-depth: 0 # This is set to download the full git history for the repo
- - name: Replace Sentry DSN and other keys
- shell: pwsh
+ - name: Get Commit Date (as ISO8601)
+ id: commit_date_iso8601
+ shell: bash
+ env:
+ TZ: UTC0
run: |
- ./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
- ./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
+ echo "date=$(git --no-pager show -s --date='format-local:%Y-%m-%dT%H:%M:%SZ' --format=%cd ${{ github.sha }})" >> "$GITHUB_OUTPUT"
- - name: Get release version
+ - name: Get Current Version
id: release_info
uses: revam/gh-action-get-tag-and-version@v1
with:
@@ -38,57 +45,54 @@ jobs:
prefix: v
prefixRegex: "[vV]?"
- - name: Setup dotnet
- uses: actions/setup-dotnet@v3
+ - id: commit_info
+ name: Shorten Commit Hash
+ uses: actions/github-script@v6
with:
- dotnet-version: ${{ matrix.dotnet }}
+ script: |
+ const sha = context.sha.substring(0, 7);
+ core.setOutput("sha", sha);
- - run: dotnet publish -c Release --no-self-contained Shoko.CLI /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="\"channel=stable,commit=${{ github.sha }},tag=${{ steps.release_info.outputs.tag }},date=${{ steps.release_info.outputs.date }},\""
-
- - name: Archive Release
- shell: pwsh
- run: Compress-Archive .\\Shoko.Server\\bin\\Release\\net8.0\\publish .\\Shoko.CLI_Framework_any-x64.zip
-
- - name: Upload Release
- uses: svenstaro/upload-release-action@v2
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN }}
- file: ./Shoko.CLI*.zip
- tag: ${{ steps.release_info.outputs.tag }}
- file_glob: true
-
- cli-standalone-stable:
+ cli-release:
runs-on: ubuntu-latest
+ needs:
+ - current_info
+
strategy:
matrix:
- rid: ['win-x64', 'linux-x64', 'linux-arm64']
- dotnet: [ '8.x' ]
-
- name: Build CLI — Standalone ${{ matrix.rid }} (Stable)
+ dotnet:
+ - '8.x'
+ include:
+ - build_type: 'Standalone'
+ build_props: ''
+ display_id: 'linux-x64'
+ rid: 'linux-x64'
+ - build_type: 'Standalone'
+ build_props: ''
+ display_id: 'linux-arm64'
+ rid: 'linux-arm64'
+ - build_type: 'Framework'
+ build_props: '--no-self-contained'
+ display_id: 'any-x64'
+ rid: 'linux-x64'
+
+ name: Build CLI — ${{ matrix.build_type }} ${{ matrix.display_id }} (Release)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
- ref: "${{ github.ref }}"
+ ref: "${{ github.sha }}"
submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
- name: Replace Sentry DSN and other keys
shell: pwsh
run: |
./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ ./.github/workflows/ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- - name: Get release version
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
- with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
-
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
@@ -100,67 +104,53 @@ jobs:
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release -r ${{ matrix.rid }} Shoko.CLI /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="\"channel=stable,commit=${{ github.sha }},tag=${{ steps.release_info.outputs.tag }},date=${{ steps.release_info.outputs.date }},\""
+ - run: dotnet publish -c Release -r ${{ matrix.rid }} -f net8.0 ${{ matrix.build_props }} Shoko.CLI /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="\"channel=stable,commit=${{ needs.current_info.outputs.sha }},tag=${{ needs.current_info.outputs.tag }},date=${{ needs.current_info.outputs.date }},\""
- - name: Archive Release (${{ matrix.rid }})
+ - name: Archive Release
shell: pwsh
- run: Compress-Archive .\\Shoko.Server\\bin\\Release\\net8.0\\${{ matrix.rid }}\\publish .\\Shoko.CLI_Standalone_${{ matrix.rid }}.zip
+ run: Compress-Archive .\\Shoko.Server\\bin\\Release\\net8.0\\publish .\\Shoko.CLI_${{ matrix.build_type }}_${{ matrix.display_id }}.zip
- - name: Upload Release (${{ matrix.rid }})
+ - name: Upload Release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./Shoko.CLI*.zip
- tag: ${{ steps.release_info.outputs.tag }}
+ tag: ${{ needs.current_info.outputs.tag }}
file_glob: true
- - name: Upload Artifact to shokoanime.com
- if: ${{ matrix.rid != 'win-x64' }}
- shell: pwsh
- env:
- FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
- FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
- FTP_SERVER: ${{ secrets.FTP_SERVER }}
- run:
- .\\.github\\workflows\\UploadRelease.ps1 -remote "ShokoServer-${{ steps.release_info.outputs.version_short }}-${{ matrix.rid }}.zip" -local "Shoko.CLI_Standalone_${{ matrix.rid }}.zip";
+ tray-service-framework:
+ runs-on: windows-latest
- tray-service-framework-stable:
- runs-on: windows-2022
+ needs:
+ - current_info
strategy:
matrix:
- dotnet: [ '8.x' ]
+ dotnet:
+ - '8.x'
name: Build Tray Service — Framework dependent (Stable)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.ref }}"
submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
- name: Replace Sentry DSN and other keys
shell: pwsh
run: |
.\\.github\\workflows\\ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ .\\.github\\workflows\\ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
.\\.github\\workflows\\ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- - name: Get release version
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
- with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
-
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release -r win-x64 --no-self-contained Shoko.TrayService /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="channel=stable%2ccommit=${{ github.sha }}%2ctag=${{ steps.release_info.outputs.tag }}%2cdate=${{ steps.release_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
+ - run: dotnet publish -c Release -r win-x64 --no-self-contained Shoko.TrayService /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="channel=stable%2ccommit=${{ github.sha }}%2ctag=${{ needs.current_info.outputs.tag }}%2cdate=${{ needs.current_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
- name: Archive Release
shell: pwsh
@@ -171,46 +161,42 @@ jobs:
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./Shoko.TrayService*.zip
- tag: ${{ steps.release_info.outputs.tag }}
+ tag: ${{ needs.current_info.outputs.tag }}
file_glob: true
- tray-service-standalone-stable:
- runs-on: windows-2022
+ tray-service-installer:
+ runs-on: windows-latest
+
+ needs:
+ - current_info
strategy:
matrix:
- dotnet: [ '8.x' ]
+ dotnet:
+ - '8.x'
- name: Build Tray Service — Standalone (Stable)
+ name: Build Tray Service — Installer (Stable)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.ref }}"
submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
- name: Replace Sentry DSN and other keys
shell: pwsh
run: |
.\\.github\\workflows\\ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ .\\.github\\workflows\\ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
.\\.github\\workflows\\ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- - name: Get release version
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
- with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
-
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release -r win-x64 --self-contained true -f net8.0-windows Shoko.TrayService /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="channel=stable%2ccommit=${{ github.sha }}%2ctag=${{ steps.release_info.outputs.tag }}%2cdate=${{ steps.release_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
+ - run: dotnet publish -c Release -r win-x64 --self-contained true Shoko.TrayService /p:Version="${{ needs.current_info.outputs.version }}" /p:InformationalVersion="channel=stable%2ccommit=${{ github.sha }}%2ctag=${{ needs.current_info.outputs.tag }}%2cdate=${{ needs.current_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
- name: Archive Release
shell: pwsh
@@ -221,7 +207,7 @@ jobs:
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./Shoko.TrayService*.zip
- tag: ${{ steps.release_info.outputs.tag }}
+ tag: ${{ needs.current_info.outputs.tag }}
file_glob: true
- name: Build Installer
@@ -232,46 +218,162 @@ jobs:
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./Shoko.Setup.exe
- tag: ${{ steps.release_info.outputs.tag }}
+ tag: ${{ needs.current_info.outputs.tag }}
file_glob: true
- - name: Upload Installer to shokoanime.com
+ - name: Upload Installer to site
shell: pwsh
env:
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
FTP_SERVER: ${{ secrets.FTP_SERVER }}
run:
- .\\.github\\workflows\\UploadRelease.ps1 -remote "ShokoServer-${{ steps.release_info.outputs.version_short }}-Win.exe" -local "Shoko.Setup.exe";
+ .\\.github\\workflows\\UploadRelease.ps1 -remote "ShokoServer-${{ needs.current_info.outputs.version_short }}-Win.exe" -local "Shoko.Setup.exe";
- sentry-upload:
+ docker-release-build:
runs-on: ubuntu-latest
needs:
- - cli-framework-stable
- - cli-standalone-stable
- - tray-service-framework-stable
- - tray-service-standalone-stable
+ - current_info
- name: Upload version info to Sentry.io
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: 'amd64'
+ dockerfile: 'Dockerfile'
+
+ - arch: 'arm64'
+ dockerfile: 'Dockerfile.aarch64'
+
+ name: Build Docker Image - ${{ matrix.arch }} (Release)
steps:
- - name: Checkout master
+ - name: Checkout "${{ github.ref }}"
uses: actions/checkout@master
with:
ref: "${{ github.ref }}"
submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
- - name: Get release version
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
+ - name: Replace Sentry DSN and other keys
+ shell: pwsh
+ run: |
+ ./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ ./.github/workflows/ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
+ ./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
+
+ - uses: docker/setup-qemu-action@v2
+ name: Set up QEMU
with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
+ platforms: arm64
+ if: ${{ matrix.arch == 'arm64' }}
+
+ - uses: docker/setup-buildx-action@v2
+ name: Set up Docker Buildx
+
+ - uses: docker/login-action@v2
+ name: Log into GitHub Container Registry
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: docker/login-action@v2
+ name: Log into Docker Hub
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ # Disabled provenance for now, until it works with docker manifest create.
+ # The manifest list produced by the new feature is incompatible with the
+ # expected format used in the docker manifest create command.
+ - uses: docker/build-push-action@v4
+ name: Build and Push the Docker image
+ with:
+ context: .
+ file: ${{ matrix.dockerfile }}
+ push: true
+ tags: |
+ ghcr.io/${{ secrets.DOCKER_REPO }}:latest-${{ matrix.arch }}
+ ghcr.io/${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag }}-${{ matrix.arch }}
+ ghcr.io/${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag_major }}-${{ matrix.arch }}
+ ghcr.io/${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag_minor }}-${{ matrix.arch }}
+ ${{ secrets.DOCKER_REPO }}:latest-${{ matrix.arch }}
+ ${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag }}-${{ matrix.arch }}
+ ${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag_major }}-${{ matrix.arch }}
+ ${{ secrets.DOCKER_REPO }}:${{ needs.current_info.outputs.tag_minor }}-${{ matrix.arch }}
+ platforms: linux/${{ matrix.arch }}
+ build-args: |
+ version=${{ needs.current_info.outputs.version }}
+ channel=stable
+ commit=${{ github.sha }}
+ date=${{ needs.current_info.outputs.date }}
+ tag=${{ needs.current_info.outputs.tag }}
+ provenance: false
+
+ docker-release-push_manifest:
+ runs-on: ubuntu-latest
+
+ needs:
+ - current_info
+ - docker-release-build
+
+ name: Push combined tag "${{ matrix.tag }}" for both images
+
+ strategy:
+ fail-fast: false
+ matrix:
+ tag:
+ - latest
+ - ${{ needs.current_info.outputs.tag }}
+ - ${{ needs.current_info.outputs.tag_major }}
+ - ${{ needs.current_info.outputs.tag_minor }}
+
+ steps:
+ - uses: docker/login-action@v2
+ name: Log into GitHub Container Registry
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: docker/login-action@v2
+ name: Log into Docker Hub
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Create manifest
+ run: |
+ docker manifest create ghcr.io/${{ secrets.DOCKER_REPO }}:${{ matrix.tag }} --amend ghcr.io/${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}-amd64 --amend ghcr.io/${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}-arm64
+ docker manifest create ${{ secrets.DOCKER_REPO }}:${{ matrix.tag }} --amend ${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}-amd64 --amend ${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}-arm64
+
+ - name: Push manifest
+ run: |
+ docker manifest push ghcr.io/${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}
+ docker manifest push ${{ secrets.DOCKER_REPO }}:${{ matrix.tag }}
+
+ sentry-upload:
+ runs-on: ubuntu-latest
+
+ needs:
+ - current_info
+ - cli-release
+ - tray-service-framework
+ - tray-service-installer
+ - docker-release-build
+ - docker-release-push_manifest
+
+ name: Upload version info to Sentry.io
+
+ steps:
+ - name: Checkout "${{ github.ref }}"
+ uses: actions/checkout@master
+ with:
+ ref: "${{ github.ref }}"
+ submodules: recursive
- - name: Push Sentry release "${{ steps.release_info.outputs.version }}"
+ - name: Push Sentry release "${{ needs.current_info.outputs.version }}"
uses: getsentry/action-release@v1.2.1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -280,4 +382,4 @@ jobs:
# SENTRY_URL: https://sentry.io/
with:
environment: 'stable'
- version: ${{ steps.release_info.outputs.version }}
+ version: ${{ needs.current_info.outputs.version }}
diff --git a/.github/workflows/cleanup-docker-images.yml b/.github/workflows/cleanup-docker-images.yml
new file mode 100644
index 000000000..15819e683
--- /dev/null
+++ b/.github/workflows/cleanup-docker-images.yml
@@ -0,0 +1,18 @@
+name: Cleanup untagged docker images
+
+on:
+ workflow_dispatch:
+ schedule:
+ # Schedule to run at 00:50 every Sunday
+ - cron: '50 0 * * 0'
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/delete-package-versions@v5
+ with:
+ package-name: 'server'
+ package-type: 'container'
+ min-versions-to-keep: 50
+ delete-only-untagged-versions: 'true'
diff --git a/.github/workflows/docker-manual.yml b/.github/workflows/docker-manual.yml
index 8f16fe703..3799d70fd 100644
--- a/.github/workflows/docker-manual.yml
+++ b/.github/workflows/docker-manual.yml
@@ -44,6 +44,7 @@ jobs:
shell: pwsh
run: |
./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ ./.github/workflows/ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- uses: docker/setup-qemu-action@v2
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
deleted file mode 100644
index 27969be2f..000000000
--- a/.github/workflows/docker-release.yml
+++ /dev/null
@@ -1,160 +0,0 @@
-name: Publish to Docker Hub (Release)
-
-on:
- release:
- types:
- - published
- branches: master
-
-jobs:
- docker-release-build:
- name: Build docker image
- strategy:
- matrix:
- include:
- - arch: 'amd64'
- dockerfile: 'Dockerfile'
-
- - arch: 'arm64'
- dockerfile: 'Dockerfile.aarch64'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@master
- with:
- ref: "${{ github.ref }}"
- submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
-
- - name: Replace Sentry DSN and other keys
- shell: pwsh
- run: |
- ./.github/workflows/ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
- ./.github/workflows/ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
-
- - name: Get release info
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
- with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
-
- - uses: docker/setup-qemu-action@v2
- name: Set up QEMU
- with:
- platforms: arm64
- if: ${{ matrix.arch == 'arm64' }}
-
- - uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
-
- - uses: docker/login-action@v2
- name: Log into GitHub Container Registry
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - uses: docker/login-action@v2
- name: Log into Docker Hub
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
-
- # Disabled provenance for now, until it works with docker manifest create.
- # The manifest list produced by the new feature is incompatible with the
- # expected format used in the docker manifest create command.
- - uses: docker/build-push-action@v4
- name: Build and Push the Docker image
- with:
- context: .
- file: ${{ matrix.dockerfile }}
- push: true
- tags: |
- ghcr.io/${{ secrets.DOCKER_REPO }}:latest-${{ matrix.arch }}
- ghcr.io/${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-${{ matrix.arch }}
- ${{ secrets.DOCKER_REPO }}:latest-${{ matrix.arch }}
- ${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-${{ matrix.arch }}
- platforms: linux/${{ matrix.arch }}
- build-args: |
- version=${{ steps.release_info.outputs.version }}
- channel=stable
- commit=${{ github.sha }}
- date=${{ steps.release_info.outputs.date }}
- tag=${{ steps.release_info.outputs.tag }}
- provenance: false
-
- docker-release-push_manifest_latest:
- runs-on: ubuntu-latest
- name: Push combined latest tag for both images
- needs:
- - docker-release-build
-
- steps:
- - uses: docker/login-action@v2
- name: Log into GitHub Container Registry
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - uses: docker/login-action@v2
- name: Log into Docker Hub
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
-
- - name: Create manifest
- run: |
- docker manifest create ghcr.io/${{ secrets.DOCKER_REPO }}:latest --amend ghcr.io/${{ secrets.DOCKER_REPO }}:latest-amd64 --amend ghcr.io/${{ secrets.DOCKER_REPO }}:latest-arm64
- docker manifest create ${{ secrets.DOCKER_REPO }}:latest --amend ${{ secrets.DOCKER_REPO }}:latest-amd64 --amend ${{ secrets.DOCKER_REPO }}:latest-arm64
-
- - name: Push manifest
- run: |
- docker manifest push ghcr.io/${{ secrets.DOCKER_REPO }}:latest
- docker manifest push ${{ secrets.DOCKER_REPO }}:latest
-
- docker-release-push_manifest_version:
- runs-on: ubuntu-latest
- name: Push combined versioned tag for both images
- needs:
- - docker-release-build
-
- steps:
- - uses: actions/checkout@master
- with:
- ref: "${{ github.ref }}"
- submodules: recursive
- fetch-depth: 0 # This is set to download the full git history for the repo
-
- - name: Get release info
- id: release_info
- uses: revam/gh-action-get-tag-and-version@v1
- with:
- tag: "${{ github.ref }}"
- prefix: v
- prefixRegex: "[vV]?"
-
- - uses: docker/login-action@v2
- name: Log into GitHub Container Registry
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - uses: docker/login-action@v2
- name: Log into Docker Hub
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
-
- - name: Create manifest
- run: |
- docker manifest create ghcr.io/${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }} --amend ghcr.io/${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-amd64 --amend ghcr.io/${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-arm64
- docker manifest create ${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }} --amend ${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-amd64 --amend ${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}-arm64
-
- - name: Push manifest
- run: |
- docker manifest push ghcr.io/${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}
- docker manifest push ${{ secrets.DOCKER_REPO }}:${{ steps.release_info.outputs.tag }}
diff --git a/.github/workflows/issue-no-response.yml b/.github/workflows/issue-no-response.yml
index eafdc86fd..d3cc853bd 100644
--- a/.github/workflows/issue-no-response.yml
+++ b/.github/workflows/issue-no-response.yml
@@ -6,8 +6,8 @@ on:
issue_comment:
types: [created]
schedule:
- # Schedule for five minutes after the hour, every 6th hour
- - cron: '5 */6 * * *'
+ # Schedule to run at 00:50 every Monday
+ - cron: '50 0 * * 1'
jobs:
noResponse:
diff --git a/.github/workflows/tray-standalone-manual.yml b/.github/workflows/tray-standalone-manual.yml
index 69c8fefe6..4ada685df 100644
--- a/.github/workflows/tray-standalone-manual.yml
+++ b/.github/workflows/tray-standalone-manual.yml
@@ -38,6 +38,7 @@ jobs:
shell: pwsh
run: |
.\\.github\\workflows\\ReplaceSentryDSN.ps1 -dsn ${{ secrets.SENTRY_DSN }}
+ .\\.github\\workflows\\ReplaceTmdbApiKey.ps1 -apiKey ${{ secrets.TMDB_API }}
.\\.github\\workflows\\ReplaceAVD3URL.ps1 -url ${{ secrets.AVD3_URL }}
- name: Get release info
@@ -53,7 +54,7 @@ jobs:
with:
dotnet-version: ${{ matrix.dotnet }}
- - run: dotnet publish -c Release -r win-x64 --self-contained true -f net8.0-windows Shoko.TrayService /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="channel=${{ github.event.inputs.release }}%2ccommit=${{ github.sha }}%2cdate=${{ steps.release_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
+ - run: dotnet publish -c Release -r win-x64 --self-contained true Shoko.TrayService /p:Version="${{ steps.release_info.outputs.version }}" /p:InformationalVersion="channel=${{ github.event.inputs.release }}%2ccommit=${{ github.sha }}%2cdate=${{ steps.release_info.outputs.date }}%2c" # %2c is comma, blame windows/pwsh
- uses: actions/upload-artifact@v3
with:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 1275ae579..341883b4b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,11 +14,12 @@
"args": [],
"cwd": "${workspaceFolder}/Shoko.Server/bin/Debug/net8.0",
"env": {
- "SHOKO_HOME": "${workspaceFolder}/Shoko.Server/bin/Debug/net8.0/data"
+ "SHOKO_HOME": "${workspaceFolder}/Shoko.Server/bin/Data"
},
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "integratedTerminal",
- "stopAtEntry": false
+ "stopAtEntry": false,
+ "requireExactSource": false
},
{
"name": ".NET Core Attach",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 70ee8ef47..7f9abd40a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,49 @@
{
+ "cSpell.words": [
+ "allcinema",
+ "animeshon",
+ "anison",
+ "APIV2Helper",
+ "automagically",
+ "avdump",
+ "avdumping",
+ "bangumi",
+ "cabac",
+ "clob",
+ "Cloneable",
+ "crunchyroll",
+ "fanart",
+ "fanarts",
+ "favourite",
+ "flac",
+ "funimation",
+ "imdb",
+ "IUDPConnectionHandler",
+ "k1itsu",
+ "kanareading",
+ "magick",
+ "malid",
+ "muxed",
+ "muxing",
+ "mylist",
+ "outro",
+ "ova",
+ "ovas",
+ "rescan",
+ "rewatching",
+ "seasonwide",
+ "seiyuu",
+ "shoko",
+ "shokofin",
+ "signalr",
+ "subsampling",
+ "syoboi",
+ "theora",
+ "tvdb",
+ "vndb",
+ "vndbid",
+ "vorbis",
+ "webui"
+ ],
"dotnet.defaultSolution": "Shoko.Server.sln"
}
diff --git a/Dependencies/MediaInfo/History.txt b/Dependencies/MediaInfo/History.txt
index 19a650b43..a747cbfb4 100644
--- a/Dependencies/MediaInfo/History.txt
+++ b/Dependencies/MediaInfo/History.txt
@@ -12,6 +12,402 @@ Known bugs
- Languages (other than english and French) : not all words are translated, I need translators!
- Others? https://sourceforge.net/p/mediainfo/_list/tickets
+Version 24.06, 2024-06-27
+-------------
++ I1881, MXF & MOV: customizable seek pos and duration of caption probe
++ I1882, CEA-608/708: option for forcing all CC1-CC4/T1 if stream is detected
++ JPEG 2000: support of HTJ2K profile
++ JPEG 2000: readout of jp2h colr atom, more file extensions, better support of broken files
++ DAT: Support of raw Digital Audio Tape
++ Enable Control Flow Guard (CFG) and Control-flow Enforcement Technology (CET)
++ Conformance checker: an element is indicated bigger than its upper element
++ Conformance checker: option for max count of items per check
+x Windows GUI: Fix unwanted deactivation of the ffmpeg plugin
+x I2086, MXF: StreamOrder for tracks in ANC
+x I2076, Dolby E: StreamOrder includes all underlying streams
+x I2087, MPEG-TS: general duration includes before and after PCR offsets
+x WavPack: various fixes for multichannel & DSD files
+x Supported platforms: this is the last version compatible with RHEL/CentOS 7, SLE 12, Debian 10, Mageia 8
+
+Version 24.05, 2024-05-30
+-------------
++ I2029, MXF: decode of VBI (Line 21 & VITC)
++ I2058, VorbisCom: show MusicBrainz IDs in XML or full text output
++ I1881, MXF & MOV: customizable seek pos and duration of caption probe
++ I2005, WavPack: support of non-standard sampling rate
++ I2021, MP4: support of Qt style AudioSampleEntry in ISO MP4
++ Conformance checker: report of malformed frames for AVC & HEVC & AAC
++ Conformance checker: an element is indicated bigger than its upper element
++ Conformance checker: Add more stream synchronization related checks
++ Conformance checker: Check coherency of MXF elements having vectors
++ Conformance checker: check of MPEG Audio sync loss in raw MP3 & truncated file
++ Conformance checker: FFV1 checks also when in AVI and MOV/MP4
++ Conformance checker: check if a TIFF file is complete
++ Conformance checker: span of frames & frame/timestamp/byte offset
+x Avoid infinite loop with distant files
+x MXF: Support of SMPTE ST 422-2019 I2
+x I2055, Dolby Vision: fix crash with some files
+x I2054, ID3v2: fix crash with some malformed files
+x FFV1: fix conformance checker crash with Golomb Rice parsing
+x AC-3: fix crash with some TrueHD files
+x I2005, WavPack: handle of small files
+x BMP: fix bitdepth info
+
+Version 24.04, 2024-04-18
+-------------
++ ADM: more AdvSS Emission profile checks
++ AC-3 & Dolby E: more AC-3 metadata readouts
++ AV1: support of chroma_sample_position
++ I1999, WAV: support of BS.2088 BW64 chunkId
++ I2008, Wavpack: support of DSD
++ I1882, CEA-608/708: options for ignoring command only streams
++ I1990, FLV: support of enhanced RTMP
+x WAV: fix support of 4+ GB ADM
+x I2005, WavPack: fix duration with small files
+x I2009, IVF: fix division by zero with buggy files
+
+Version 24.03, 2024-03-28
+-------------
++ ADM: ADM v3, including profile element, support
++ ADM: conformance checks on AdvSS Emission profile
++ Dolby E: display more AC-3 metadata items
++ MOV/MP4: parsing of rtmd (real time metadata) tracks
++ PNG: packing kind (linear or indexed)
+x WAV: support of 4+ GiB axml (useful for huge ADM content)
+x MPEG-H: fix uninitialized values leading to random behavior
+x PDF: fix crash with corrupted files
+x MOV/MP4: fix bit depth info for some PCM tracks with pcmC box
+
+Version 24.01, 2024-01-31
+-------------
++ ADM: Dolby Atmos Master ADM Profile conformance checker (technology preview)
++ Dolby Vision: support of version 3, with compression info, and profile 20
++ Dolby Vision: explicit display of profile
++ HEVC: support of multiview profile signaled in VPS extension
++ MP4: parsing of vexu (Video Extended Usage) box
++ ICC: support of CCIP in ICC in JPEG, PNG, TIFF, MP4, raw files
++ MPEG-TS: detection of VVC and EVC
++ AVC: count of slices
++ PNG: support of color description chunks (CCIP CLLI MDCV)
++ GXF: support of AVC and VC-3
++ TrueHD: display of Dolby Surround EX & Dolby Pro Logic IIz
+x Matroska: better fallback in case of buggy timecode
+x I1940, MOV/MP4: fix slowness with some unrecognized metadata atoms
+x HDR10/HDR10+: fix HDR10 info even if some characteristics are not met
+
+Version 23.11, 2023-11-30
+-------------
++ XMP: support of a couple of additional metadata
++ PNG: pixel aspect ratio, gamma, active bit depth
++ PNG: support of textual metadata
++ Detection of active width/height/DAR (based on FFmpeg), Windows only
++ Matroska: show ST-12 timecode of first frame
++ ADM: rounding of FFoA to 0 decimal and Start/End time codes to 2 decimals
++ WAV: support of big (1+ GB) axml chunks
++ ADM: support of big (1+ GB) files on 32-bit systems
+x I1876, BWF: fix missing precision in TimeReference export
+x I1607, MPEG-TS/PS: Less Inform() with Open(memory) than Open(file)
+x MP4/MOV: show right time code of last frame with complex time code tracks
+x Duration: timecode output should not use drop frame for 23.976fps
+x AVC+HEVC: fix handling of DF timestamps
+x SF1188, ID3v2: fix wrong handling of chunks having padding
+x I1887, TS DVB: fix wrong handling of UTF-8 strings in service name
+x I1892, Matroska: fix date readout if before the millennium
+
+Version 23.10, 2023-10-04
+-------------
++ Italian language update
++ Languages: add 'fil' (Filipino)
++ Support of MPEG-H in MPEG-TS
++ MOV/MP4: caption probing time expanded from ~15s to ~30s
++ MPEG-7 and DVD-Video: provide title duration based on frame rate
++ WAV: better display of buggy WAV files have 2 fmt/data chunks
+x MOV/MP4: fix lack of detection of CEA-608/708 if junk at end of stream
+x DVD-Video: fix duration if more than 1 menu
+
+Version 23.09, 2023-09-14
+-------------
++ DTS-UHD support (contribution from Xperi)
++ MPEG-7 output update, supporting collections for DVD Video
++ ISO 9660: more metadata
++ AVC: read out of time code
+x DVD Video: better support of ISO having several episodes
+x MPEG Video: fix duration not including last field duration (interlaced content only)
+x I754, AVC&HEVC: fix risk of crash with some streams
+
+Version 23.07, 2023-07-12
+-------------
++ USAC conformance checker: update DRC presence check
++ USAC conformance checker: sbgp presence check
++ USAC conformance checker: difference between extra zero bytes and other extra bytes
++ ISO 9660: support of DVD video, with option for listing all contents
++ MPEG-7: support of collections (beta)
++ More Blackmagic RAW meta kinds
++ DTS-HD: DTSHDHDR header support (used for raw DTS-HD files)
+x ADIF: fix wrong detection of lot of files as ADIF (Android and MediaInfoOnline)
+x USAC conformance checker: fix arith context handling in some corner cases
+x ADM: some tweaks about FFoA/Start/End time codes
+x Remove curl default ca info message in stdout
+
+Version 23.06, 2023-06-28
+-------------
++ USAC/xHE-AAC conformance checker
++ S-ADM: support of SMPTE ST 2127-1 / SMPTE ST 2109 / SMPTE ST 2127-10 (S-ADM in MGA in MXF)
++ S-ADM: add S-ADM version and support of 1/1.001 frame rates
++ ADM: show FFoA/Start/End as timestamp and timecode
++ MPEG-7 output update with more extensions
++ MPEG-TS: support of JPEG XS
++ DTS-UHD: support of DTS-UHD (a.k.a. DTS-X P2) in MP4
++ MP4: detection of VVC
++ MP4: support of media characteristicd (spoken dialog, translation, easy to read...)
++ MP4: support of more Blackmagic RAW Codec IDs
++ MP4: support of ipcm CodecID
++ MP4: support of service kind
++ HEVC: support of SMPTE ST 2094-10
++ HDR: display of all formats if more than 1 format is detected
++ Matroska: support of SMPTE ST 12 in block additions
++ HEVC: time code SEI readout
++ AVC & HEVC: active format description readout
++ MPEG-TS: support of SMPTE ST 2038 (ancillary data)
+x ADM/Dolby: fix wrong FFoA with 1.001 frame rates
++ MOV/MP4: more info with tracks having unknown type
+x MOV/MP4: avoid to parse too much content with non stripped timecodes
+x MOV/MP4: avoid incoherent behavior if 2 tracks have the same ID
+x TTML: fix default frame rate
+x TimeCode: 1/1.001 frame rate was not always detected
+x MediaTrace: fix some random blank outputs
+x URL: remove query part of the URL in the FileExtension field
+x Referenced files: fix handling of URL encoded with UTF-8 content
+x Matroska: fix crash in support of HDR10+
+
+Version 23.04, 2023-04-26
+-------------
++ MXF: support of SMPTE ST 381-4 (AAC in MXF)
++ DTS: show MA or HRA tip in commercial name for DTS:X
++ DTS: detection of DTS:X not lossless
++ APT-X100 a.k.a. Cinema DTS: initial support
++ Matroska: support of HDR10+
++ MP4: more information about thumbnails
++ ID3v2: more information about thumbnails
++ VP9: initial support, for more information about chroma subsampling
++ AWS S3: support for reference files with AccessID:SecretKey@URL
++ Windows: fix some download errors with AWS S3 objects (libcurl update)
+x AWS S3: fix errors with some special chars in SecretKey
+x AWS S3: fix random credential issues with non geolocated URLs
+x DTS: fix freeze with some DTS-HD not DTS:X files
+x MPEG-TS: fix crash in HEVC_timing_and_HRD
+x AAC: fix samples per frame with SBR streams
+x FLAC: fix missing Tbc Tbr in ChannelLayout
+
+Version 23.03, 2023-03-29
+-------------
++ DTS: Detection of IMAX Enhanced
++ MOV/MP4: Add HDR Vivid format support
++ HEVC: Add HDR Vivid format support
++ MXF/PCM: detect silent tracks (full parsing only)
++ Monkey's Audio: support of 32-bit files, show version
++ MP4 audioProfileLevelIndication: add Low Delay AAC v2 Profile
++ MP4/MOV: support of FLAC
++ MOV/MP4: support of TTML with images
++ MPEG-7: 3 modes (strict, relaxed, extended)
++ MPEG-7: more sub-termIDs (AudioPresentationCS)
++ MPEG-7: Add more PublicIdentifiers
++ MPEG-7: more sub-termIDs (MP4, WAV, AVC, ProRes)
++ AVI/WAV: display of the kind of fmt chunk
++ AVC: detection of more profiles
++ ChannelLayout: difference between M (Mono) and C (Center, part of multichannel content)
++ AC-3: detection of channel layout also for encrypted content
++ AC-4 and MPEG-H 3D Audio: Merged channel layout (all sub-streams together)
++ DTS: Detection of real bit depth e.g. 20 instead of only byte aligned bit depth (16 or 24)
++ FLAC: support of BWF in Vorbis comments
++ N19/STL: codepage, subtitle count, max line per subtitle, more metadata
++ ISAN: detection of descriptions referencing an ISAN
++ AAC: detection of eSBR (and fix of random wrong PS detection)
++ Extract of time codes, XML format, currently only for for MXF
+x MP4/MOV: fix freezes with some unknown udta atoms
+x FLV: fix duration of 0 with some buggy files
+x AVC: fix PTS of last frame
+x FFV1: fix potential crash with malformed files
+x AV1: add HDR format line and fix HDR values
+x AAC and WAV: fix of channel layout display for 5 front channels
+x AC-4: Tl/Tr mapped to to Tsl/Tsr
+x FLAC: fix sampling count
+x ID3v2: fix Genre not showing ID 0 (Blues)
+x MPEG-7: VBR fix
+x JSON/XML: Remove minus sign from element names
+x Normalization of date/time in report
+
+Version 22.12, 2022-12-22
+-------------
++ WebVTT: more information (duration, start/end timestamp, count of lines...)
++ MP4/MOV: support of FLAC
++ MP4/MOV: support of LanguageIETF
++ ProRes: parse FFmpeg glbl atom for getting color range
++ AVI/WAV: detection of character set
++ WAV: display MD5 of raw content
++ FLAC: display MD5 of unencoded content
++ USAC: trace of UsacFrame() up to after preroll
++ MOV/MP4: option for parsing only the header, no parsing of any frame
++ MXF: option for parsing only the header, no parsing of any frame
+x MXF: quicker parsing when fast parsing is requested
+x I662, WAV: fix false-positive detection of DTS in PCM
+x I1637, MPEG-Audio: proper support of Helix MP3 encoder detection and encoder settings
+x I661, MXF: fix UKDPP FpaPass value sometimes not outputted
+x S1182, Teletext subtitle: prioritize subtitle metadata other overs
+x Matroska: Better handling in case of buggy AVC stream
+x 22.2 audio: Fix name of 1 channel (Tll --> Tsl)
+x AAC: fix wrong parsing of some bitstreams
+x Fix crash with stdin input and ctrl-c
+x Fix memory leak in JSON output
+
+Version 22.09, 2022-10-04
+-------------
++ Italian language update
++ USAC: IOD and sampling rate coherency checking
++ ADM: support of nested objects and complementary objects
++ AC-4: Display of Custom downmix targets
++ IAB: Parsing of IAB bitstream and ADM-like output
++ Frame rate: store FrameRate_Num/Den also for integer values
++ MPEG-4/MOV: support of time codes >30 fps
++ MOV/MPEG-4: List of QuickTime time code discontinuities
++ Dolby Vision: add info about more profiles
+x Text streams: show stream frame rate if not same as container frame rate
+x CDP: fix rounding of frame rate
+x SCC: fix of CEA-608 FirstDisplay_Delay_Frames
+x SCC: fix TimeCode_Last
+x MPEG-4/MOV: last time code value for all kind of QuickTime time codes
+x MOV/MPEG-4: Fix frame count for NDF non-integer frame rates
+x JSON: fix invalid output in some corner cases
+x Several other parsing bug/crash fixes (thanks to fuzzing by users)
+
+Version 22.06, 2022-06-23
+-------------
++ MXF: FFV1 support
++ Dolby Vision: add info about more profiles
++ AAC: check of missing ID_END and incoherent count of channels
++ NSV: better handling of buggy StarDiva agenda negative timestamps
++ Text: Show text frame rate
++ Text: frame rate precise numerator/denominator also for text streams
++ CDP: readout of display aspect ratio
++ MPEG-4/MOV: support of time codes >30 fps
++ TTML: Support of more timeExpression flavors
+x ADM: correctly map Dolby binaural render mode to track UID
+x Dolby Audio Metadata: first frame of action in HH:MM:SS:FF format
+x Dolby Vision: profiles and levels in decimal rather than in hexadecimal
+x MXF: fix of Dolby Vision Metadata not displayed if HDR10 metadata is present
+x MPEG-4/MOV: avoid buggy frame rates by taking frame rate from stts atom
+x CDP: better catching of wrong line21_field value
+x NSV: better handling of invalid frames
+x MXF: Include frame count in SDTI and SystemScheme1 time codes to time stamp conversion
+x TTML: do not show frame rate if it is from MediaInfo options
+x DV: timecode trace in HH:MM:SS:FF format
+
+Version 22.03, 2022-03-31
+-------------
++ NSV (Nullsoft Video): full featured support
++ NSV: support of proprietary StarDiva metadata (by reverse engineering)
++ HEVC: CEA-608/708 support
++ Dolby Audio Metadata: First frame of action, binaural render modes
++ Dolby Audio Metadata: 5.1 and 5.1.x downmix, 5.1 to 2.0 downmix, associated video frame rate, trim modes
++ MOV/MP4, TTML, SCC, MXF TC: time code of last frame
++ EIA-608: first displayed caption type
++ EIA-608: Maximum count of lines per event and total count of lines
++ EIA-608: duration of the visible content
++ TTML: Total count of lines
++ TTML: Maximum count of lines per event (including overlapping times)
++ TTML: Frame count, display aspect ratio
++ TTML: Support of timestamps in frames
++ SCC: Delay
++ Matroska: Encoding settings metadata support
++ MOV/MP4: Gamma metadata output
++ MPEG-4/MOV: difference between audio Center and Mono when possible
++ MP4/MOV: Support of dec3 atom in wave atom
++ MPEG-4/MOV: show both values in case of chan atom ChannelLayoutTag / ChannelDescriptions mismatch
++ MP4/MOV: Support of dec3 atom in wave atom
++ MXF: better support of AVC streams without SPS/PPS
++ ADM: display channel index of trackUIDs
+x WAV: fix freeze with 32-bit PCM
+x DPX: fix regression with DPX files more than 64 MB
+x Dolby E: fix crash with some invalid streams
+x E-AC-3: service kind was not correctly handled
+x EXR: fix of bad handling of files with long names in attributes
+x TTML: correct handling of 29.97 DF time codes
+x AV1: fix of the parsing of some streams, especially the ones with HDR metadata
+x WebVTT: was not correctly handling WebVTT header with comment
+x Matroska: fix false positive detection of bad CRC32
+x Several other parsing bug/crash fixes
+
+Version 21.09, 2021-09-17
+-------------
++ Graph view for 3D audio streams (thanks to graphviz)
++ ADM: full featured support (programmes, content, objects, pack formats...)
++ ADM: in WAV (axml, bxml), MXF
++ S-ADM in AES3: support of Levels A1 and AX1
++ MOV/MP4: support of Dolby Vision Metadata XML
++ MXF: detection of IAB
++ SMPTE ST 337 (AES3): support of subframe mode
++ HEVC: CEA-608/708 caption support
++ MP4/QuickTime: Android slow motion real frame rate
++ JSON output: add creatingLibrary field
+x MPEG-4: read too much data with some predecessor definitions
+x EBUCore: fix of fields order and types
+
+Version 21.03, 2021-03-26
+-------------
++ WAV: ADM profile detection of Dolby Atmos Master or MPEG-H
++ SMPTE ST 337: support of AC-4
++ AC-3/AC-4: show top layer channels after Lw/Rw, as it becomes the defacto standard layout
++ Dolby Surround EX and Pro Logic IIz detection
++ Matroska: add DV support
++ CLI: read from stdin
++ DV: remove check of zeroed bytes in timecode, considered again as valid timecode
++ TIFF; add support of compression codes 7 and 8
++ WAV: show bext (BWF) version in verbose mode / XML / JSON
++ MXF: detection fo DCI P3 mastering display color primaries
++ Options: add software version to text output
++ Options: add report creation timestamp to text output
++ macOS: native build for Apple Silicon (arm64)
+x HDR: mastering max. luminance precision was wrong
+x WM: fix EncodingTime parsing
+x MOV/MP4: skip XMP huge atoms, fix
+x MPEG-TS: fix inverted supplementary_audio_descriptor mix_type values
+x AAC: fix File_Aac::is_intensity according to ISO/IEC 14496-3:2009
+x I1353, MP4: Skip user data Xtra and free atoms
+x FFV1: fix crash with some bitstreams parsing
+x TIFF: fix division by 0
+x RF64: fix the WAV malformed chunk size test
+x Supported platforms: this is the last version compatible with Windows XP, macOS 10.5-10.9, RHEL/CentOS 6
+
+Version 20.09, 2020-10-09
+-------------
++ Dolby ED2: full featured support (presentations, presentation targets, beds, objects)
++ MKV: support of Dolby Vision metadata
++ MXF: detection of Dolby E hidden in PCM tracks having more than 2 channels
++ WAV: detection of Dolby E hidden in PCM tracks having more than 2 channels
++ CineForm: display of color space (including Bayer), bit depth
+x WAV: more precise sample count
+x SMPTE ST 337: catch of streams starting later than usual (probing increased from 4 to 16 PCM "frames")
+x PNG: detection of additional alpha plane in color space
+x MXF: detection of additional alpha plane in color space
+x AVI: detection of additional alpha plane in color space
+x MPEG Audio: was wrongly flagging Xing info tag as CBR
+x VorbisTag: does not skip DISCID
+x Miscellaneous bug/crash fixes
+
+Version 20.08, 2020-08-11
+-------------
++ MPEG-H 3D Audio full featured support (group presets, switch groups, groups, signal groups)
++ MP4/MOV: support of more metadata locations
++ JSON and XML outputs: authorize "complete" output
++ MPEG-4: support of TrueHD
++ WM: show legacy value of performer if not same as modern one
++ WAV: trace of adtl (Associated Data List) chunk
+x URL encoding detection fix for URL having a query part (issue with e.g. pre-signed AWS S3 URLs)
+x Don't try to seek to the end (false positive range related error with HTTP)
+x DPX: don't load the whole file in RAM
+x Opus: fix wrong channel mapping
+x Miscellaneous other bug fixes
+
Version 20.03, 2020-04-03
-------------
+ AC-4 full featured support (presentations, groups, substreams)
diff --git a/Dependencies/MediaInfo/LIBCURL.DLL b/Dependencies/MediaInfo/LIBCURL.DLL
index 09233612c..9a94a3e09 100644
Binary files a/Dependencies/MediaInfo/LIBCURL.DLL and b/Dependencies/MediaInfo/LIBCURL.DLL differ
diff --git a/Dependencies/MediaInfo/LICENSE b/Dependencies/MediaInfo/LICENSE
index e078aa27d..4169054b5 100644
--- a/Dependencies/MediaInfo/LICENSE
+++ b/Dependencies/MediaInfo/LICENSE
@@ -1,6 +1,6 @@
BSD 2-Clause License
-Copyright (c) 2002-2020, MediaArea.net SARL
+Copyright (c) 2002-2024, MediaArea.net SARL
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/Dependencies/MediaInfo/License.html b/Dependencies/MediaInfo/License.html
index 53a6afeed..4255d64a9 100644
--- a/Dependencies/MediaInfo/License.html
+++ b/Dependencies/MediaInfo/License.html
@@ -9,7 +9,7 @@
MediaInfo(Lib) License
- Copyright (c) 2002-2020 MediaArea.net SARL . All rights reserved.
+ Copyright (c) 2002-2024 MediaArea.net SARL . All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
@@ -17,12 +17,10 @@
MediaInfo(Lib) License
Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
+ this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation and/or
- other materials provided with the distribution.
-
+ this list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
@@ -39,21 +37,19 @@
MediaInfo(Lib) License
-
Alternate open source licenses:
- You can relicense (including source headers change) MediaInfo
- under Apache License 2.0 or later,
- and/or GNU Lesser General Public License 2.1 or later,
- and/or GNU General Public License 2.0 or later,
- and/or Mozilla Public License 2.0 or later.
+
Alternate open source licenses:
+You can relicense (including source headers change) MediaInfo
+under Apache License 2.0 or later,
+and/or GNU Lesser General Public License 2.1 or later,
+and/or GNU General Public License 2.0 or later,
+and/or Mozilla Public License 2.0 or later.
-
Alternate license for redistributions of the library in binary form:
- Redistributions in binary form must reproduce the following sentence (including the link to the website) in the
- documentation and/or other materials provided with the distribution.
- This product uses MediaInfo library, Copyright (c) 2002-2020 MediaArea.net SARL .
+
Alternate license for redistributions of the library in binary form:
+Redistributions in binary form must reproduce the following sentence (including the link to the website) in the documentation and/or other materials provided with the distribution.
+This product uses MediaInfo library, Copyright (c) 2002-2024 MediaArea.net SARL .
@@ -62,32 +58,31 @@
Third party libraries
The software relies on third party libraries. Such libraries have their own license:
-
+
Contributors
- Jérôme Martinez (main developper)
+ Jérôme Martinez (main developer)
Lionel Duchateau (odd formats support)
XhmikosR from MPC-HC Team (tests)
FlylinkDC++ team (tests, crash corrections)
- Max Pozdeev (former native Mac GUI developper)
+ Max Pozdeev (former native Mac GUI developer)
diff --git a/Dependencies/MediaInfo/MediaInfo.exe b/Dependencies/MediaInfo/MediaInfo.exe
index caf861033..7be44d498 100644
Binary files a/Dependencies/MediaInfo/MediaInfo.exe and b/Dependencies/MediaInfo/MediaInfo.exe differ
diff --git a/Dockerfile b/Dockerfile
index 8d939ba6c..daaabe40b 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,8 +25,10 @@ ARG channel
RUN apt-get update && apt-get install -y gnupg curl
-RUN curl https://mediaarea.net/repo/deb/debian/pubkey.gpg | apt-key add -
-RUN echo "deb https://mediaarea.net/repo/deb/debian/ bookworm main" | tee -a /etc/apt/sources.list
+RUN curl --retry 3 -O https://mediaarea.net/repo/deb/debian/pubkey.gpg
+# This converts the old format gpg key to the new format. The old format cannot be used with [signed-by] in the apt sources.list file.
+RUN gpg --no-default-keyring --keyring ./temp-keyring.gpg --import pubkey.gpg && gpg --no-default-keyring --keyring ./temp-keyring.gpg --export --output /usr/share/keyrings/mediainfo.gpg && rm pubkey.gpg
+RUN echo "deb [signed-by=/usr/share/keyrings/mediainfo.gpg] https://mediaarea.net/repo/deb/debian/ bookworm main" | tee -a /etc/apt/sources.list
RUN apt-get update && apt-get install -y apt-utils gosu jq unzip mediainfo librhash-dev
@@ -52,4 +54,4 @@ HEALTHCHECK --start-period=5m CMD curl -s -H "Content-Type: application/json" -H
EXPOSE 8111
-ENTRYPOINT /bin/bash /dockerentry.sh
+ENTRYPOINT ["/bin/bash", "/dockerentry.sh"]
diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64
index a41581c79..d17dfec9b 100644
--- a/Dockerfile.aarch64
+++ b/Dockerfile.aarch64
@@ -26,8 +26,10 @@ ARG channel
RUN apt-get update && apt-get install -y gnupg curl
-RUN curl https://mediaarea.net/repo/deb/debian/pubkey.gpg | apt-key add -
-RUN echo "deb https://mediaarea.net/repo/deb/debian/ bookworm main" | tee -a /etc/apt/sources.list
+RUN curl --retry 3 -O https://mediaarea.net/repo/deb/debian/pubkey.gpg
+# This converts the old format gpg key to the new format. The old format cannot be used with [signed-by] in the apt sources.list file.
+RUN gpg --no-default-keyring --keyring ./temp-keyring.gpg --import pubkey.gpg && gpg --no-default-keyring --keyring ./temp-keyring.gpg --export --output /usr/share/keyrings/mediainfo.gpg && rm pubkey.gpg
+RUN echo "deb [signed-by=/usr/share/keyrings/mediainfo.gpg] https://mediaarea.net/repo/deb/debian/ bookworm main" | tee -a /etc/apt/sources.list
RUN apt-get update && apt-get install -y apt-utils gosu jq unzip mediainfo librhash-dev
@@ -53,4 +55,4 @@ HEALTHCHECK --start-period=5m CMD curl -s -H "Content-Type: application/json" -H
EXPOSE 8111
-ENTRYPOINT /bin/bash /dockerentry.sh
+ENTRYPOINT ["/bin/bash", "/dockerentry.sh"]
diff --git a/Installer/ShokoServer.iss b/Installer/ShokoServer.iss
index 1c1ea54dc..9ffa51a18 100644
--- a/Installer/ShokoServer.iss
+++ b/Installer/ShokoServer.iss
@@ -1,8 +1,8 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-#define AppVer GetVersionNumbersString('..\Shoko.Server\bin\Release\net8.0-windows\win10-x64\ShokoServer.exe')
-#define AppSlug Copy(StringChange(AppVer, ".", ""), 1, Len(AppVer) - 1)
+#define AppVer GetVersionNumbersString('..\Shoko.Server\bin\Release\net8.0-windows\win-x64\ShokoServer.exe')
+#define AppSlug Copy(StringChange(AppVer, ".", "-"), 1, Len(AppVer) - 2)
#define MyAppExeName "ShokoServer.exe"
[Setup]
@@ -45,7 +45,7 @@ Name: "{commonstartup}\Shoko Server"; Filename: "{app}\ShokoServer.exe"; Tasks:
[Run]
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""Shoko Server - Client Port"" dir=in action=allow protocol=TCP localport=8111"; Flags: runhidden; StatusMsg: "Open exception on firewall..."; Tasks: Firewall
Filename: "{app}\ShokoServer.exe"; Flags: nowait postinstall skipifsilent shellexec; Description: "{cm:LaunchProgram,Shoko Server}"
-Filename: "https://docs.shokoanime.com/server/install/"; Flags: shellexec runasoriginaluser postinstall; Description: "Shoko Server Install Guide"
+Filename: "https://docs.shokoanime.com/getting-started/running-shoko-server/"; Flags: shellexec runasoriginaluser postinstall; Description: "Shoko Server Install Guide"
Filename: "https://shokoanime.com/blog/shoko-version-{#AppSlug}-released/"; Flags: shellexec runasoriginaluser postinstall; Check: BlogPostCheck; Description: "View {#AppVer} Release Notes"
[UninstallRun]
@@ -60,7 +60,7 @@ Name: "{commonappdata}\ShokoServer"; Permissions: users-full
[Files]
Source: ".\FixPermissions.bat"; DestDir: "{app}"; Flags: ignoreversion
-Source: "..\Shoko.Server\bin\Release\net8.0-windows\win10-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "..\Shoko.Server\bin\Release\net8.0-windows\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Code]
diff --git a/README.md b/README.md
index 9053f539d..5d4766058 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,8 @@ Shoko takes the hassle out of managing your anime collection. With its user-frie
back and let it do the work for you. No more manual inputting or renaming - just effortless organization and access to
your favorite anime.
-[Learn More About Shoko](https://shokoanime.com)
+[Learn More About Shoko](https://shokoanime.com)
+[User Docs](https://docs.shokoanime.com/getting-started/installing-shoko-server)
# Supported Media Players
Shoko currently supports the following media players.
@@ -29,4 +30,26 @@ Discord, and we'll be more than happy to provide guidance and assistance.
# Building Shoko
+Install the latest .net sdk
+## Windows:
+Build TrayService or CLI from VS Code or command line via:
+
+`dotnet build Shoko.TrayService/Shoko.TrayService.csproj`
+
+## Linux:
+Install mediainfo and rhash. For apt, that would be:
+
+`sudo apt install mediainfo librhash-dev`
+
+
+Build from CLI:
+
+`dotnet build -c=Release -r linux-x64 -f net8.0 Shoko.CLI/Shoko.CLI.csproj`
+
+If that doesn't work, this document may be out of date. Check the dockerfile for guaranteedly updated build steps.
+
+# Contributing
+We are always accepting help, and there are a million little things that always need done. Hop on our [discord](https://discord.gg/vpeHDsg) and talk to us. Communication is important in any team. No offesnse, but it's difficult to help anyone that shows up out of nowhere, opens 3 issues, then creates a PR without even talking to us. We have a wealth of experience. Let us help you...preferably before the ADHD takes over, you hyperfixate, and you come up with a fantastic solution to problem that isn't at all what you expected. Support is also best found in the discord, in case you read this far.
+
+![Alt](https://repobeats.axiom.co/api/embed/c233a2de69d1f2f56e4cbe96b4b4cd33dc223d19.svg "Repobeats analytics image")
diff --git a/Shoko.CLI/Shoko.CLI.csproj b/Shoko.CLI/Shoko.CLI.csproj
index 1f8cfc819..a4c34118b 100644
--- a/Shoko.CLI/Shoko.CLI.csproj
+++ b/Shoko.CLI/Shoko.CLI.csproj
@@ -1,6 +1,7 @@
- net8.0;net8.0-windows
+ net8.0
+ win-x64;linux-x64
exe
x64;AnyCPU
false
@@ -13,12 +14,6 @@
false
enable
-
- win-x64
-
-
- win-x64;linux-x64
-
..\Shoko.Server\bin\Debug\
@@ -46,4 +41,4 @@
db.ico
-
\ No newline at end of file
+
diff --git a/Shoko.Commons b/Shoko.Commons
index 259c93adc..e257946e1 160000
--- a/Shoko.Commons
+++ b/Shoko.Commons
@@ -1 +1 @@
-Subproject commit 259c93adc13e82c5bfc760aac8eb354c5ce0b7fd
+Subproject commit e257946e182c409130102d34374c41a6f8d77ddb
diff --git a/Shoko.Plugin.Abstractions/Attributes/PluginSettings.cs b/Shoko.Plugin.Abstractions/Attributes/PluginSettings.cs
index 3d3e8ca10..f41c62b29 100644
--- a/Shoko.Plugin.Abstractions/Attributes/PluginSettings.cs
+++ b/Shoko.Plugin.Abstractions/Attributes/PluginSettings.cs
@@ -1,10 +1,8 @@
using System;
-namespace Shoko.Plugin.Abstractions.Attributes
-{
+namespace Shoko.Plugin.Abstractions.Attributes;
- public class PluginSettingsAttribute : Attribute
- {
-
- }
-}
\ No newline at end of file
+///
+/// An attribute for defining a plugin settings object.
+///
+public class PluginSettingsAttribute : Attribute { }
diff --git a/Shoko.Plugin.Abstractions/Attributes/RenamerAttribute.cs b/Shoko.Plugin.Abstractions/Attributes/RenamerAttribute.cs
deleted file mode 100644
index cac071e45..000000000
--- a/Shoko.Plugin.Abstractions/Attributes/RenamerAttribute.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-
-namespace Shoko.Plugin.Abstractions.Attributes
-{
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
- public class RenamerAttribute : Attribute
- {
- public RenamerAttribute(string renamerId)
- {
- RenamerId = renamerId;
- }
-
- public string RenamerId { get; }
- private string _desc;
-
- public string Description
- {
- get => _desc ?? RenamerId;
- set => _desc = value;
- }
- }
-}
\ No newline at end of file
diff --git a/Shoko.Plugin.Abstractions/Attributes/RenamerIDAttribute.cs b/Shoko.Plugin.Abstractions/Attributes/RenamerIDAttribute.cs
new file mode 100644
index 000000000..954a58fc2
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Attributes/RenamerIDAttribute.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Shoko.Plugin.Abstractions.Attributes;
+
+///
+/// This attribute is used to identify a renamer.
+/// It is an attribute to allow getting the ID of the renamer without instantiating it.
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+public class RenamerIDAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID of the renamer.
+ public RenamerIDAttribute(string renamerId)
+ {
+ RenamerId = renamerId;
+ }
+
+ ///
+ /// The ID of the renamer.
+ ///
+ public string RenamerId { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/Attributes/RenamerSettingAttribute.cs b/Shoko.Plugin.Abstractions/Attributes/RenamerSettingAttribute.cs
new file mode 100644
index 000000000..301797592
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Attributes/RenamerSettingAttribute.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Runtime.CompilerServices;
+using Shoko.Plugin.Abstractions.Enums;
+
+namespace Shoko.Plugin.Abstractions.Attributes;
+
+///
+/// An attribute for defining a renamer setting on a renamer settings object.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public class RenamerSettingAttribute : Attribute
+{
+ ///
+ /// The name of the setting to be displayed
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// The type of the setting, to be used in the UI
+ ///
+ public RenamerSettingType Type { get; set; }
+
+ ///
+ /// The language to use for text highlighting in the editor
+ ///
+ public CodeLanguage Language { get; set; }
+
+ ///
+ /// The description of the setting and what it controls
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Create a new setting definition for a property.
+ ///
+ /// The name of the setting to be displayed. Will be inferred if not specified.
+ /// The type of the setting, to be used in the UI. Will be inferred if not specified.
+ /// The description of the setting and what it controls. Can be omitted.
+ public RenamerSettingAttribute([CallerMemberName] string? name = null, RenamerSettingType type = RenamerSettingType.Auto, string? description = null)
+ {
+ // the nullability is suppressed because [CallerMemberName] is used
+ Name = name!;
+ Type = type;
+ Description = description;
+ }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/AniDBMediaData.cs b/Shoko.Plugin.Abstractions/DataModels/AniDBMediaData.cs
index 11fbf6c95..a55f9f341 100644
--- a/Shoko.Plugin.Abstractions/DataModels/AniDBMediaData.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/AniDBMediaData.cs
@@ -1,10 +1,19 @@
using System.Collections.Generic;
-namespace Shoko.Plugin.Abstractions.DataModels
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Represents the audio and subtitle languages associated with an AniDB file.
+///
+public class AniDBMediaData
{
- public class AniDBMediaData
- {
- public IReadOnlyList AudioLanguages { get; set; }
- public IReadOnlyList SubLanguages { get; set; }
- }
-}
\ No newline at end of file
+ ///
+ /// Gets the audio languages associated with the media file.
+ ///
+ public IReadOnlyList AudioLanguages { get; set; } = new List();
+
+ ///
+ /// Gets the subtitle languages associated with the media file.
+ ///
+ public IReadOnlyList SubLanguages { get; set; } = new List();
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/AnimeTitle.cs b/Shoko.Plugin.Abstractions/DataModels/AnimeTitle.cs
index e770ed32d..1be4add30 100644
--- a/Shoko.Plugin.Abstractions/DataModels/AnimeTitle.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/AnimeTitle.cs
@@ -1,150 +1,41 @@
-using System.Xml.Serialization;
+
using Shoko.Plugin.Abstractions.Enums;
-namespace Shoko.Plugin.Abstractions.DataModels
-{
- public class AnimeTitle
- {
- public DataSourceEnum Source { get; set; }
+namespace Shoko.Plugin.Abstractions.DataModels;
- public TitleLanguage Language { get; set; }
+///
+/// Represents a title from a data source.
+///
+public class AnimeTitle
+{
+ ///
+ /// The source.
+ ///
+ public DataSourceEnum Source { get; set; }
- public string LanguageCode { get; set; }
+ ///
+ /// The language.
+ ///
+ public TitleLanguage Language { get; set; }
- public string Title { get; set; }
+ ///
+ /// The language code.
+ ///
+ public string LanguageCode { get; set; } = string.Empty;
- public TitleType Type { get; set; }
- }
+ ///
+ /// The country code, if available and applicable.
+ ///
+ public string? CountryCode { get; set; }
- public enum TitleLanguage
- {
- Unknown = 0,
- English = 1,
- Romaji,
- Japanese,
- Afrikaans,
- Arabic,
- Bangladeshi,
- Bulgarian,
- FrenchCanadian,
- Czech,
- Danish,
- German,
- Greek,
- Spanish,
- Estonian,
- Finnish,
- French,
- Galician,
- Hebrew,
- Hungarian,
- Italian,
- Korean,
- Lithuanian,
- Mongolian,
- Malaysian,
- Dutch,
- Norwegian,
- Polish,
- Portuguese,
- BrazilianPortuguese,
- Romanian,
- Russian,
- Slovak,
- Slovenian,
- Serbian,
- Swedish,
- Thai,
- Turkish,
- Ukrainian,
- Vietnamese,
- Chinese,
- ChineseSimplified,
- ChineseTraditional,
- Pinyin,
- Latin,
- Albanian,
- Basque,
- Bengali,
- Bosnian,
- Amharic,
- Armenian,
- Azerbaijani,
- Belarusian,
- Catalan,
- Chichewa,
- Corsican,
- Croatian,
- Divehi,
- Esperanto,
- Fijian,
- Georgian,
- Gujarati,
- HaitianCreole,
- Hausa,
- Icelandic,
- Igbo,
- Indonesian,
- Irish,
- Javanese,
- Kannada,
- Kazakh,
- Khmer,
- Kurdish,
- Kyrgyz,
- Lao,
- Latvian,
- Luxembourgish,
- Macedonian,
- Malagasy,
- Malayalam,
- Maltese,
- Maori,
- Marathi,
- MyanmarBurmese,
- Nepali,
- Oriya,
- Pashto,
- Persian,
- Punjabi,
- Quechua,
- Samoan,
- ScotsGaelic,
- Sesotho,
- Shona,
- Sindhi,
- Sinhala,
- Somali,
- Swahili,
- Tajik,
- Tamil,
- Tatar,
- Telugu,
- Turkmen,
- Uighur,
- Uzbek,
- Welsh,
- Xhosa,
- Yiddish,
- Yoruba,
- Zulu,
- }
+ ///
+ /// The title value.
+ ///
+ public string Title { get; set; } = string.Empty;
- public enum TitleType
- {
- [XmlEnum("none")]
- None = 0,
- [XmlEnum("main")]
- Main = 1,
- [XmlEnum("official")]
- Official = 2,
- [XmlEnum("short")]
- Short = 3,
- [XmlEnum("syn")]
- Synonym = 4,
- [XmlEnum("card")]
- TitleCard = 5,
- [XmlEnum("kana")]
- KanjiReading = 6,
- }
+ ///
+ /// The type.
+ ///
+ public TitleType Type { get; set; }
}
+
diff --git a/Shoko.Plugin.Abstractions/DataModels/AnimeType.cs b/Shoko.Plugin.Abstractions/DataModels/AnimeType.cs
index c7f554370..4a2b0f26a 100644
--- a/Shoko.Plugin.Abstractions/DataModels/AnimeType.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/AnimeType.cs
@@ -1,13 +1,38 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Type of series.
+///
public enum AnimeType
{
+ ///
+ /// A movie. A self-contained story.
+ ///
Movie = 0,
+
+ ///
+ /// An original Video Animation (OVA). A short series of episodes, not broadcast on TV.
+ ///
OVA = 1,
+
+ ///
+ /// A TV series. A series of episodes that are broadcast on TV.
+ ///
TVSeries = 2,
+
+ ///
+ /// A TV special. A special episode of a TV series.
+ ///
TVSpecial = 3,
+
+ ///
+ /// A web series. A series of episodes that are released on the web.
+ ///
Web = 4,
+
+ ///
+ /// Other misc. types of series not listed in this enum.
+ ///
Other = 5,
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/DropFolderType.cs b/Shoko.Plugin.Abstractions/DataModels/DropFolderType.cs
index 0f1da5746..ef4213b02 100644
--- a/Shoko.Plugin.Abstractions/DataModels/DropFolderType.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/DropFolderType.cs
@@ -1,16 +1,30 @@
using System;
-namespace Shoko.Plugin.Abstractions.DataModels
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// The rules that this Import Folder should adhere to. A folder that is both a Source and Destination cares not how files are moved in or out of it.
+///
+[Flags]
+public enum DropFolderType
{
///
- /// The rules that this Import Folder should adhere to. A folder that is both a Source and Destination cares not how files are moved in or out of it.
+ /// None.
+ ///
+ Excluded = 0,
+
+ ///
+ /// Source.
+ ///
+ Source = 1,
+
+ ///
+ /// Destination.
+ ///
+ Destination = 2,
+
+ ///
+ /// Both Source and Destination.
///
- [Flags]
- public enum DropFolderType
- {
- Excluded = 0,
- Source = 1,
- Destination = 2,
- Both = Source | Destination
- }
-}
\ No newline at end of file
+ Both = Source | Destination,
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/EpisodeCounts.cs b/Shoko.Plugin.Abstractions/DataModels/EpisodeCounts.cs
index cd2e7ebe6..b6fe3dc0a 100644
--- a/Shoko.Plugin.Abstractions/DataModels/EpisodeCounts.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/EpisodeCounts.cs
@@ -1,13 +1,51 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Represents the count of different types of episodes.
+///
public class EpisodeCounts
{
+ ///
+ /// The number of normal episodes.
+ ///
public int Episodes { get; set; }
+
+ ///
+ /// The number of special episodes.
+ ///
public int Specials { get; set; }
+
+ ///
+ /// The number of credits episodes.
+ ///
public int Credits { get; set; }
+
+ ///
+ /// The number of trailer episodes.
+ ///
public int Trailers { get; set; }
- public int Others { get; set; }
+
+ ///
+ /// The number of parody episodes.
+ ///
public int Parodies { get; set; }
+
+ ///
+ /// The number of other episodes.
+ ///
+ public int Others { get; set; }
+
+ ///
+ /// Returns the number of episodes for the given
+ ///
+ public int this[EpisodeType type] => type switch
+ {
+ EpisodeType.Episode => Episodes,
+ EpisodeType.Special => Specials,
+ EpisodeType.Credits => Credits,
+ EpisodeType.Trailer => Trailers,
+ EpisodeType.Parody => Parodies,
+ _ => Others
+ };
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/EpisodeType.cs b/Shoko.Plugin.Abstractions/DataModels/EpisodeType.cs
index 653d634da..ec4e8c20d 100644
--- a/Shoko.Plugin.Abstractions/DataModels/EpisodeType.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/EpisodeType.cs
@@ -1,14 +1,37 @@
-using System;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Episode type.
+///
public enum EpisodeType
{
+ ///
+ /// Normal episode.
+ ///
Episode = 1,
+
+ ///
+ /// Credits. Be it opening credits or ending credits.
+ ///
Credits = 2,
+
+ ///
+ /// Special episode.
+ ///
Special = 3,
+
+ ///
+ /// Trailer.
+ ///
Trailer = 4,
+
+ ///
+ /// Parody.
+ ///
Parody = 5,
+ ///
+ /// Other.
+ ///
Other = 6
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IAniDBFile.cs b/Shoko.Plugin.Abstractions/DataModels/IAniDBFile.cs
index 9122c6fa9..ef599a733 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IAniDBFile.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IAniDBFile.cs
@@ -1,44 +1,54 @@
using System;
-namespace Shoko.Plugin.Abstractions.DataModels
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// AniDB file information.
+///
+public interface IAniDBFile
{
- public interface IAniDBFile
- {
- ///
- /// The ID of the file on AniDB
- ///
- int AniDBFileID { get; }
- ///
- /// Info about the release group of the file
- ///
- IReleaseGroup ReleaseGroup { get; }
- ///
- /// Where the file was ripped from, bluray, dvd, etc
- ///
- string Source { get; }
- ///
- /// The Filename as released, according to AniDB. It's usually correct.
- ///
- string OriginalFilename { get; }
- ///
- /// Description of the file on AniDB. This will often be blank, and it's generally not useful
- ///
- string Description { get; }
- ///
- /// When the file was released, according to AniDB. This will be wrong for a lot of older or less popular anime
- ///
- DateTime? ReleaseDate { get; }
- ///
- /// Usually 1. Sometimes 2. 3 happens. It's incremented when a release is updated due to errors
- ///
- int Version { get; }
- ///
- /// This is mostly for hentai, and it's often wrong.
- ///
- bool Censored { get; }
- ///
- /// AniDB's user input data for streams
- ///
- AniDBMediaData MediaInfo { get; }
- }
-}
\ No newline at end of file
+ ///
+ /// The ID of the file on AniDB
+ ///
+ int AniDBFileID { get; }
+
+ ///
+ /// Info about the release group of the file
+ ///
+ IReleaseGroup ReleaseGroup { get; }
+
+ ///
+ /// Where the file was ripped from, blu-ray, dvd, etc
+ ///
+ string Source { get; }
+
+ ///
+ /// The Filename as released, according to AniDB. It's usually correct.
+ ///
+ string OriginalFilename { get; }
+
+ ///
+ /// Description of the file on AniDB. This will often be blank, and it's generally not useful
+ ///
+ string Description { get; }
+
+ ///
+ /// When the file was released, according to AniDB. This will be wrong for a lot of older or less popular anime
+ ///
+ DateTime? ReleaseDate { get; }
+
+ ///
+ /// Usually 1. Sometimes 2. 3 happens. It's incremented when a release is updated due to errors
+ ///
+ int Version { get; }
+
+ ///
+ /// This is mostly for restricted entries, and it's often wrong.
+ ///
+ bool Censored { get; }
+
+ ///
+ /// AniDB's user input data for streams
+ ///
+ AniDBMediaData MediaInfo { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IAnime.cs b/Shoko.Plugin.Abstractions/DataModels/IAnime.cs
deleted file mode 100644
index 82e49f8fe..000000000
--- a/Shoko.Plugin.Abstractions/DataModels/IAnime.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions.DataModels;
-
-public interface IAnime : ISeries
-{
- #region To-be-removed
-
- ///
- /// Relations for the anime.
- ///
- [Obsolete("Use ISeries.RelatedSeries instead.")]
- IReadOnlyList Relations { get; }
-
- ///
- /// The number of total episodes in the series.
- ///
- [Obsolete("Use ISeries.EpisodeCounts instead.")]
- EpisodeCounts EpisodeCounts { get; }
-
- ///
- /// The AniDB Anime ID.
- ///
- [Obsolete("Use ID instead.")]
- int AnimeID { get; }
-
- #endregion
-}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IEpisode.cs b/Shoko.Plugin.Abstractions/DataModels/IEpisode.cs
index 2a7330406..bc3813766 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IEpisode.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IEpisode.cs
@@ -1,16 +1,24 @@
using System;
using System.Collections.Generic;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
-public interface IEpisode : IWithTitles, IWithDescriptions, IMetadata
+///
+/// Episode metadata.
+///
+public interface IEpisode : IWithTitles, IWithDescriptions, IWithImages, IMetadata
{
///
- /// The AniDB Anime ID.
+ /// The series id.
///
int SeriesID { get; }
+ ///
+ /// The shoko episode ID, if we have any.
+ ///
+ IReadOnlyList ShokoEpisodeIDs { get; }
+
///
/// The episode type.
///
@@ -39,12 +47,12 @@ public interface IEpisode : IWithTitles, IWithDescriptions, IMetadata
///
/// Get the series info for the episode, if available.
///
- ISeries? SeriesInfo { get; }
+ ISeries? Series { get; }
///
- /// All episodes linked to this entity.
+ /// All shoko episodes linked to this episode.
///
- IReadOnlyList LinkedEpisodes { get; }
+ IReadOnlyList ShokoEpisodes { get; }
///
/// All cross-references linked to the episode.
@@ -55,32 +63,4 @@ public interface IEpisode : IWithTitles, IWithDescriptions, IMetadata
/// Get all videos linked to the episode, if any.
///
IReadOnlyList VideoList { get; }
-
- #region To-be-removed
-
- ///
- /// The AniDB Episode ID.
- ///
- [Obsolete("Use ID instead.")]
- int EpisodeID { get; }
-
- ///
- /// The AniDB Anime ID.
- ///
- [Obsolete("Use ShowID instead.")]
- int AnimeID { get; }
-
- ///
- /// The runtime of the episode, in seconds.
- ///
- [Obsolete("Use Runtime instead.")]
- int Duration { get; }
-
- ///
- ///
- ///
- [Obsolete("Use EpisodeNumber instead.")]
- int Number { get; }
-
- #endregion
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IGroup.cs b/Shoko.Plugin.Abstractions/DataModels/IGroup.cs
deleted file mode 100644
index ca78c6e23..000000000
--- a/Shoko.Plugin.Abstractions/DataModels/IGroup.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Collections.Generic;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions.DataModels;
-
-public interface IGroup
-{
- ///
- /// Group Name
- ///
- string Name { get; }
-
- ///
- /// The series that is used for the name. May be null. Just use Series.FirstOrDefault() at that point.
- ///
- IAnime MainSeries { get; }
-
- ///
- /// The series in a group, ordered by AirDate
- ///
- IReadOnlyList Series { get; }
-}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IHashes.cs b/Shoko.Plugin.Abstractions/DataModels/IHashes.cs
index 1b4612363..369105e23 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IHashes.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IHashes.cs
@@ -1,11 +1,28 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Hash container object.
+///
public interface IHashes
{
+ ///
+ /// Gets the CRC32 hash if it's available.
+ ///
string? CRC { get; }
+
+ ///
+ /// Gets the MD5 hash if it's available.
+ ///
string? MD5 { get; }
+
+ ///
+ /// Gets the ED2K hash.
+ ///
string ED2K { get; }
+
+ ///
+ /// Gets the SHA1 hash if it's available.
+ ///
string? SHA1 { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IImageMetadata.cs b/Shoko.Plugin.Abstractions/DataModels/IImageMetadata.cs
new file mode 100644
index 000000000..020d7a8e2
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/IImageMetadata.cs
@@ -0,0 +1,119 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Shoko.Plugin.Abstractions.Enums;
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Image metadata.
+///
+public interface IImageMetadata : IMetadata, IEquatable
+{
+ ///
+ /// Image type.
+ ///
+ ImageEntityType ImageType { get; }
+
+ ///
+ /// MIME type for image.
+ ///
+ public string ContentType { get; }
+
+ ///
+ /// Indicates the image is enabled for use. Disabled images should not be
+ /// used except for administrative purposes.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Indicates that the image is the preferred image for the linked entity.
+ ///
+ ///
+ public bool IsPreferred { get; }
+
+ ///
+ /// Indicates the image is locked and cannot be removed by the user. It can
+ /// still be disabled though.
+ ///
+ public bool IsLocked { get; }
+
+ ///
+ /// Indicates the image is readily available.
+ ///
+ public bool IsAvailable { get; }
+
+ ///
+ /// Indicates that the image is readily available from the local file system.
+ ///
+ public bool IsLocalAvailable { get; }
+
+ ///
+ /// Indicates that the image is readily available from the remote location.
+ ///
+ public bool IsRemoteAvailable { get; }
+
+ ///
+ /// Image aspect ratio.
+ ///
+ ///
+ double AspectRatio { get; }
+
+ ///
+ /// Width of the image, in pixels.
+ ///
+ int Width { get; }
+
+ ///
+ /// Height of the image, in pixels.
+ ///
+ int Height { get; }
+
+ ///
+ /// Language code for the language used for the text in the image, if any.
+ /// Or null if the image doesn't contain any language specifics.
+ ///
+ string? LanguageCode { get; }
+
+ ///
+ /// The language used for any text in the image, if any.
+ /// Or if the image doesn't contain any
+ /// language specifics.
+ ///
+ TitleLanguage Language { get; }
+
+ ///
+ /// A full remote URL to fetch the image, if the provider uses remote
+ /// images.
+ ///
+ string? RemoteURL { get; }
+
+ ///
+ /// Local absolute path to where the image is stored. Will be null if the
+ /// image is currently not locally available.
+ ///
+ string? LocalPath { get; }
+
+ ///
+ /// Get a stream that reads the image contents from the local copy or remote
+ /// copy of the image. Returns null if the image is currently unavailable.
+ ///
+ ///
+ /// Allow retrieving the stream from the local cache.
+ ///
+ ///
+ /// Allow retrieving the stream from the remote cache.
+ ///
+ ///
+ /// A stream of the image content, or null. The stream will never be
+ /// interrupted partway through.
+ ///
+ Stream? GetStream(bool allowLocal = true, bool allowRemote = true);
+
+ ///
+ /// Will attempt to download the remote copy of the image available at
+ /// to the .
+ ///
+ /// Indicates that the image is available locally.
+ Task DownloadImage(bool force = false);
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IImportFolder.cs b/Shoko.Plugin.Abstractions/DataModels/IImportFolder.cs
index faabf59af..5cb610a2d 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IImportFolder.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IImportFolder.cs
@@ -1,8 +1,8 @@
-using System;
-
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Represents an import folder.
+///
public interface IImportFolder
{
///
@@ -20,18 +20,18 @@ public interface IImportFolder
///
string Path { get; }
+ ///
+ /// The available free space in the import folder.
+ ///
+ ///
+ /// A value of -1
indicates that the import folder does not
+ /// exist, while a value of -2
indicates that free space could
+ /// not be determined.
+ ///
+ long AvailableFreeSpace { get; }
+
///
/// The rules that this Import Folder should adhere to. E.g. a folder that is both a and cares not how files are moved in or out of it.
///
DropFolderType DropFolderType { get; }
-
- #region To-be-removed
-
- [Obsolete("Use ID instead.")]
- int ImportFolderID { get; }
-
- [Obsolete("Use Path instead.")]
- string Location { get; }
-
- #endregion
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IMediaContainer.cs b/Shoko.Plugin.Abstractions/DataModels/IMediaContainer.cs
deleted file mode 100644
index 3edd3fa6f..000000000
--- a/Shoko.Plugin.Abstractions/DataModels/IMediaContainer.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System.Collections.Generic;
-
-namespace Shoko.Plugin.Abstractions.DataModels
-{
- public interface IMediaContainer
- {
- IGeneralStream General { get; }
- IVideoStream Video { get; }
- IReadOnlyList Audio { get; }
- IReadOnlyList Subs { get; }
- bool Chaptered { get; }
- }
-
- public interface IStream
- {
- string Title { get; set; }
-
- int StreamOrder { get; set; }
-
- string Codec { get; set; }
-
- string CodecID { get; set; }
-
- ///
- /// The Language code (ISO 639-1 in everything I've seen) from MediaInfo
- ///
- string Language { get; set; }
-
- ///
- /// This is the 3 character language code
- /// This is mapped from the Language, it is not MediaInfo data
- ///
- string LanguageCode { get; set; }
-
- ///
- /// This is the Language Name, "English"
- /// This is mapped from the Language, it is not MediaInfo data
- ///
- string LanguageName { get; set; }
-
- bool Default { get; set; }
-
- bool Forced { get; set; }
- }
-
- public interface IGeneralStream : IStream
- {
- double Duration { get; set; }
-
- int OverallBitRate { get; set; }
-
- decimal FrameRate { get; set; }
- }
-
- public interface IVideoStream : IStream
- {
- int BitRate { get; set; }
-
- int Width { get; set; }
-
- int Height { get; set; }
-
- decimal FrameRate { get; set; }
-
- string FrameRate_Mode { get; set; }
-
- int BitDepth { get; set; }
-
- string StandardizedResolution { get; }
- string SimplifiedCodec { get; }
- }
-
- public interface IAudioStream : IStream
- {
- int Channels { get; set; }
-
- int SamplingRate { get; set; }
-
- string Compression_Mode { get; set; }
-
- int BitRate { get; set; }
-
- string BitRate_Mode { get; set; }
-
- int BitDepth { get; set; }
-
- string SimplifiedCodec { get; }
- }
-
- public interface ITextStream : IStream
- {
- ///
- /// Not From MediaInfo. Is this an external sub file
- ///
- bool External { get; set; }
-
- ///
- /// Not from MediaInfo, this is the name of the external sub file
- ///
- string Filename { get; set; }
-
- string SimplifiedCodec { get; }
- }
-}
\ No newline at end of file
diff --git a/Shoko.Plugin.Abstractions/DataModels/IMediaInfo.cs b/Shoko.Plugin.Abstractions/DataModels/IMediaInfo.cs
new file mode 100644
index 000000000..7d9373e9b
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/IMediaInfo.cs
@@ -0,0 +1,453 @@
+using System;
+using System.Collections.Generic;
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Media container metadata.
+///
+public interface IMediaInfo
+{
+ ///
+ /// General title for the media.
+ ///
+ string? Title { get; }
+
+ ///
+ /// Overall duration of the media.
+ ///
+ TimeSpan Duration { get; }
+
+ ///
+ /// Overall bit-rate across all streams in the media container.
+ ///
+ int BitRate { get; }
+
+ ///
+ /// Average frame-rate across all the streams in the media container.
+ ///
+ decimal FrameRate { get; }
+
+ ///
+ /// Date when encoding took place, if known.
+ ///
+ DateTime? Encoded { get; }
+
+ ///
+ /// Indicates the media is streaming-friendly.
+ ///
+ bool IsStreamable { get; }
+
+ ///
+ /// Common file extension for the media container format.
+ ///
+ string FileExtension { get; }
+
+ ///
+ /// The media container format name.
+ ///
+ string ContainerName { get; }
+
+ ///
+ /// The media container format version.
+ ///
+ int ContainerVersion { get; }
+
+ ///
+ /// First available video stream in the media container.
+ ///
+ IVideoStream? VideoStream { get; }
+
+ ///
+ /// Video streams in the media container.
+ ///
+ IReadOnlyList VideoStreams { get; }
+
+ ///
+ /// Audio streams in the media container.
+ ///
+ IReadOnlyList AudioStreams { get; }
+
+ ///
+ /// Sub-title (text) streams in the media container.
+ ///
+ IReadOnlyList TextStreams { get; }
+
+ ///
+ /// Chapter information present in the media container.
+ ///
+ IReadOnlyList Chapters { get; }
+
+ ///
+ /// Container file attachments.
+ ///
+ IReadOnlyList Attachments { get; }
+}
+
+///
+/// Stream metadata.
+///
+public interface IStream
+{
+ ///
+ /// Local id for the stream.
+ ///
+ int ID { get; }
+
+ ///
+ /// Unique id for the stream.
+ ///
+ string UID { get; }
+
+ ///
+ /// Stream title, if available.
+ ///
+ string? Title { get; }
+
+ ///
+ /// Stream order.
+ ///
+ int Order { get; }
+
+ ///
+ /// Indicates this is the default stream of the given type.
+ ///
+ bool IsDefault { get; }
+
+ ///
+ /// Indicates the stream is forced to be used.
+ ///
+ bool IsForced { get; }
+
+ ///
+ /// name of the language of the stream.
+ ///
+ TitleLanguage Language { get; }
+
+ ///
+ /// 3 character language code of the language of the stream.
+ ///
+ string? LanguageCode { get; }
+
+ ///
+ /// Stream codec information.
+ ///
+ IStreamCodecInfo Codec { get; }
+
+ ///
+ /// Stream format information.
+ ///
+ IStreamFormatInfo Format { get; }
+}
+
+///
+/// Stream codec information.
+///
+public interface IStreamCodecInfo
+{
+ ///
+ /// Codec name, if available.
+ ///
+ string? Name { get; }
+
+ ///
+ /// Simplified codec id.
+ ///
+ string Simplified { get; }
+
+ ///
+ /// Raw codec id.
+ ///
+ string? Raw { get; }
+}
+
+///
+/// Stream format information.
+///
+public interface IStreamFormatInfo
+{
+ ///
+ /// Name of the format used.
+ ///
+ string Name { get; }
+
+ ///
+ /// Profile name of the format used, if available.
+ ///
+ string? Profile { get; }
+
+ ///
+ /// Compression level of the format used, if available.
+ ///
+ string? Level { get; }
+
+ ///
+ /// Format settings, if available.
+ ///
+ string? Settings { get; }
+
+ ///
+ /// Known additional features enabled for the format, if available.
+ ///
+ string? AdditionalFeatures { get; }
+
+ ///
+ /// Format endianness, if available.
+ ///
+ string? Endianness { get; }
+
+ ///
+ /// Format tier, if available.
+ ///
+ string? Tier { get; }
+
+ ///
+ /// Format commercial information, if available.
+ ///
+ string? Commercial { get; }
+
+ ///
+ /// HDR format information, if available.
+ ///
+ ///
+ /// Only available for .
+ ///
+ string? HDR { get; }
+
+ ///
+ /// HDR format compatibility information, if available.
+ ///
+ ///
+ /// Only available for .
+ ///
+ string? HDRCompatibility { get; }
+
+ ///
+ /// Context-adaptive binary arithmetic coding (CABAC).
+ ///
+ ///
+ /// Only available for .
+ ///
+ bool CABAC { get; }
+
+ ///
+ /// Bi-directional video object planes (BVOP).
+ ///
+ ///
+ /// Only available for .
+ ///
+ bool BVOP { get; }
+
+ ///
+ /// Quarter-pixel motion (Qpel).
+ ///
+ ///
+ /// Only available for .
+ ///
+ bool QPel { get; }
+
+ ///
+ /// Global Motion Compensation (GMC) mode, if available.
+ ///
+ ///
+ /// Only available for .
+ ///
+ string? GMC { get; }
+
+ ///
+ /// Reference frames count, if known.
+ ///
+ ///
+ /// Only available for .
+ ///
+ int? ReferenceFrames { get; }
+}
+
+///
+/// Stream muxing information.
+///
+public interface IStreamMuxingInfo
+{
+ ///
+ /// Raw muxing mode value.
+ ///
+ public string? Raw { get; }
+}
+
+///
+/// Video stream information.
+///
+public interface IVideoStream : IStream
+{
+ ///
+ /// Width of the video stream.
+ ///
+ int Width { get; }
+
+ ///
+ /// Height of the video stream.
+ ///
+ int Height { get; }
+
+ ///
+ /// Standardized resolution.
+ ///
+ string Resolution { get; }
+
+ ///
+ /// Pixel aspect-ratio.
+ ///
+ decimal PixelAspectRatio { get; }
+
+ ///
+ /// Frame-rate.
+ ///
+ decimal FrameRate { get; }
+
+ ///
+ /// Frame-rate mode.
+ ///
+ string FrameRateMode { get; }
+
+ ///
+ /// Total number of frames in the video stream.
+ ///
+ int FrameCount { get; }
+
+ ///
+ /// Scan-type. Interlaced or progressive.
+ ///
+ string ScanType { get; }
+
+ ///
+ /// Color-space.
+ ///
+ string ColorSpace { get; }
+
+ ///
+ /// Chroma sub-sampling.
+ ///
+ string ChromaSubsampling { get; }
+
+ ///
+ /// Matrix co-efficiency.
+ ///
+ string? MatrixCoefficients { get; }
+
+ ///
+ /// Bit-rate of the video stream.
+ ///
+ int BitRate { get; }
+
+ ///
+ /// Bit-depth of the video stream.
+ ///
+ int BitDepth { get; }
+
+ ///
+ /// How the stream is muxed in the media container.
+ ///
+ IStreamMuxingInfo Muxing { get; }
+}
+
+///
+/// Audio stream metadata.
+///
+public interface IAudioStream : IStream
+{
+ ///
+ /// Number of total channels in the audio stream.
+ ///
+ int Channels { get; }
+
+ ///
+ /// A text representation of the layout of the channels available in the
+ /// audio stream.
+ ///
+ string ChannelLayout { get; }
+
+ ///
+ /// Samples per frame.
+ ///
+ int SamplesPerFrame { get; }
+
+ ///
+ /// Sampling rate of the audio.
+ ///
+ int SamplingRate { get; }
+
+ ///
+ /// Compression mode used.
+ ///
+ string CompressionMode { get; }
+
+ ///
+ /// Dialog norm of the audio stream, if available.
+ ///
+ double? DialogNorm { get; }
+
+ ///
+ /// Bit-rate of the audio-stream.
+ ///
+ int BitRate { get; }
+
+ ///
+ /// Bit-rate mode of the audio stream.
+ ///
+ string BitRateMode { get; }
+
+ ///
+ /// Bit-depth of the audio stream.
+ ///
+ int BitDepth { get; }
+}
+
+///
+/// Text stream information.
+///
+public interface ITextStream : IStream
+{
+ ///
+ /// Sub-title of the text stream.
+ ///
+ ///
+ string? SubTitle { get; }
+
+ ///
+ /// Not From MediaInfo. Is this an external sub file
+ ///
+ bool IsExternal { get; }
+
+ ///
+ /// The name of the external subtitle file if this is stream is from an
+ /// external source. This field is only sent if
+ /// is set to true
.
+ ///
+ string? ExternalFilename { get; }
+}
+
+///
+/// Chapter information.
+///
+public interface IChapterInfo
+{
+ ///
+ /// Chapter title.
+ ///
+ string Title { get; }
+
+ ///
+ /// name of the language the chapter information.
+ ///
+ TitleLanguage Language { get; }
+
+ ///
+ /// 3 character language code of the language the chapter information.
+ ///
+ string? LanguageCode { get; }
+
+ ///
+ /// Chapter timestamp.
+ ///
+ TimeSpan Timestamp { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IMetadata.cs b/Shoko.Plugin.Abstractions/DataModels/IMetadata.cs
index 0c873aa33..5348033c2 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IMetadata.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IMetadata.cs
@@ -1,14 +1,25 @@
using Shoko.Plugin.Abstractions.Enums;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Base metadata interface.
+///
public interface IMetadata
{
+ ///
+ /// The source of the metadata.
+ ///
DataSourceEnum Source { get; }
}
+///
+/// Base metadata interface with an ID.
+///
public interface IMetadata : IMetadata where TId : struct
{
+ ///
+ /// The ID of the metadata.
+ ///
TId ID { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IMovie.cs b/Shoko.Plugin.Abstractions/DataModels/IMovie.cs
new file mode 100644
index 000000000..754d58387
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/IMovie.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Movie metadata.
+///
+public interface IMovie : IWithTitles, IWithDescriptions, IWithImages, IMetadata
+{
+ ///
+ /// The shoko series ID, if we have any.
+ /// ///
+ IReadOnlyList ShokoSeriesIDs { get; }
+
+ ///
+ /// The shoko episode ID, if we have any.
+ /// ///
+ IReadOnlyList ShokoEpisodeIDs { get; }
+
+ ///
+ /// The first release date of the movie in the country of origin, if it's known.
+ ///
+ DateTime? ReleaseDate { get; }
+
+ ///
+ /// Overall user rating for the show, normalized on a scale of 1-10.
+ ///
+ double Rating { get; }
+
+ ///
+ /// Default poster for the movie.
+ ///
+ IImageMetadata? DefaultPoster { get; }
+
+ ///
+ /// All shoko episodes linked to the movie.
+ ///
+ IReadOnlyList ShokoEpisodes { get; }
+
+ ///
+ /// All shoko series linked to the movie.
+ ///
+ IReadOnlyList ShokoSeries { get; }
+
+ ///
+ /// Related series.
+ ///
+ IReadOnlyList> RelatedSeries { get; }
+
+ ///
+ /// Related movies.
+ ///
+ IReadOnlyList> RelatedMovies { get; }
+
+ ///
+ /// All cross-references linked to the episode.
+ ///
+ IReadOnlyList CrossReferences { get; }
+
+ ///
+ /// Get all videos linked to the series, if any.
+ ///
+ IReadOnlyList VideoList { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IRelatedAnime.cs b/Shoko.Plugin.Abstractions/DataModels/IRelatedAnime.cs
deleted file mode 100644
index 01308dc78..000000000
--- a/Shoko.Plugin.Abstractions/DataModels/IRelatedAnime.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions.DataModels;
-
-public interface IRelatedAnime
-{
- int RelatedAnimeID { get; }
- IAnime? RelatedAnime { get; }
- RelationType RelationType { get; }
-}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IRelatedMetadata.cs b/Shoko.Plugin.Abstractions/DataModels/IRelatedMetadata.cs
index a13f824e0..b960b5552 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IRelatedMetadata.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IRelatedMetadata.cs
@@ -1,15 +1,30 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Related metadata.
+///
public interface IRelatedMetadata
{
+ ///
+ /// Related entity id.
+ ///
int RelatedID { get; }
+ ///
+ /// Relation type.
+ ///
RelationType RelationType { get; }
}
+///
+/// Related metadata with entity.
+///
+/// Related entity type.
public interface IRelatedMetadata : IMetadata, IRelatedMetadata where TMetadata : IMetadata
{
+ ///
+ /// Related entity, if available.
+ ///
TMetadata? Related { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IReleaseGroup.cs b/Shoko.Plugin.Abstractions/DataModels/IReleaseGroup.cs
index 4cf2c5fab..61c712dbf 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IReleaseGroup.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IReleaseGroup.cs
@@ -1,8 +1,18 @@
-namespace Shoko.Plugin.Abstractions.DataModels
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Release group.
+///
+public interface IReleaseGroup : IMetadata
{
- public interface IReleaseGroup
- {
- string Name { get; }
- string ShortName { get; }
- }
-}
\ No newline at end of file
+ ///
+ /// The name of the release group.
+ ///
+ string? Name { get; }
+
+ ///
+ /// The short name of the release group.
+ ///
+ string? ShortName { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/ISeries.cs b/Shoko.Plugin.Abstractions/DataModels/ISeries.cs
index 74b9c5ebd..e175e83e1 100644
--- a/Shoko.Plugin.Abstractions/DataModels/ISeries.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/ISeries.cs
@@ -2,21 +2,18 @@
using System.Collections.Generic;
using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
-public interface ISeries : IWithTitles, IWithDescriptions, IMetadata
+///
+/// Series metadata.
+///
+public interface ISeries : IWithTitles, IWithDescriptions, IWithImages, IMetadata
{
///
/// The shoko series ID, if we have any.
/// ///
IReadOnlyList ShokoSeriesIDs { get; }
- ///
- /// The shoko group IDs, if we have any.
- ///
- IReadOnlyList ShokoGroupIDs { get; }
-
///
/// The Anime Type.
///
@@ -44,24 +41,24 @@ public interface ISeries : IWithTitles, IWithDescriptions, IMetadata
bool Restricted { get; }
///
- /// All shoko series linked to this entity.
+ /// Default poster for the series.
///
- IReadOnlyList ShokoSeries { get; }
+ IImageMetadata? DefaultPoster { get; }
///
/// All shoko series linked to this entity.
///
- IReadOnlyList ShokoGroups { get; }
+ IReadOnlyList ShokoSeries { get; }
///
- /// All series linked to this entity.
+ /// Related series.
///
- IReadOnlyList LinkedSeries { get; }
+ IReadOnlyList> RelatedSeries { get; }
///
- /// Related series.
+ /// Related movies.
///
- IReadOnlyList> RelatedSeries { get; }
+ IReadOnlyList> RelatedMovies { get; }
///
/// All cross-references linked to the series.
@@ -71,15 +68,15 @@ public interface ISeries : IWithTitles, IWithDescriptions, IMetadata
///
/// All known episodes for the show.
///
- IReadOnlyList EpisodeList { get; }
+ IReadOnlyList Episodes { get; }
///
- /// Episode counts for every episode type.
+ /// The number of total episodes in the series.
///
- IReadOnlyDictionary EpisodeCountDict { get; }
+ EpisodeCounts EpisodeCounts { get; }
///
/// Get all videos linked to the series, if any.
///
- IReadOnlyList VideoList { get; }
+ IReadOnlyList Videos { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IVideo.cs b/Shoko.Plugin.Abstractions/DataModels/IVideo.cs
index 7ef22edda..ac5434a51 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IVideo.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IVideo.cs
@@ -1,9 +1,13 @@
using System.Collections.Generic;
+using System.IO;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Video.
+///
public interface IVideo : IMetadata
{
///
@@ -34,7 +38,7 @@ public interface IVideo : IMetadata
///
/// The MediaInfo data for the file. This can be null, but it shouldn't be.
///
- IMediaContainer? MediaInfo { get; }
+ IMediaInfo? MediaInfo { get; }
///
/// All cross-references linked to the video.
@@ -44,15 +48,20 @@ public interface IVideo : IMetadata
///
/// All episodes linked to the video.
///
- IReadOnlyList EpisodeInfo { get; }
+ IReadOnlyList Episodes { get; }
///
/// All shows linked to the show.
///
- IReadOnlyList SeriesInfo { get; }
+ IReadOnlyList Series { get; }
///
/// Information about the group
///
- IReadOnlyList GroupInfo { get; }
+ IReadOnlyList Groups { get; }
+
+ ///
+ /// Get the stream for the video, if any files are still available.
+ ///
+ Stream? GetStream();
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IVideoCrossReference.cs b/Shoko.Plugin.Abstractions/DataModels/IVideoCrossReference.cs
index cb0f7726f..1c9261c6d 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IVideoCrossReference.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IVideoCrossReference.cs
@@ -1,7 +1,10 @@
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Video cross-reference.
+///
public interface IVideoCrossReference : IMetadata
{
///
@@ -21,7 +24,7 @@ public interface IVideoCrossReference : IMetadata
int AnidbEpisodeID { get; }
///
- /// AniDB anime ID. Will be available even if is
+ /// AniDB anime ID. Will be available even if is
/// not available yet.
///
int AnidbAnimeID { get; }
@@ -51,4 +54,14 @@ public interface IVideoCrossReference : IMetadata
/// The AniDB anime series, if available.
///
ISeries? AnidbAnime { get; }
+
+ ///
+ /// The Shoko episode, if available.
+ ///
+ IShokoEpisode? ShokoEpisode { get; }
+
+ ///
+ /// The Shoko series, if available.
+ ///
+ IShokoSeries? ShokoSeries { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs b/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs
index ea7a39e3f..2d032d820 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IVideoFile.cs
@@ -1,17 +1,19 @@
-using System;
+using System.IO;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Video file location.
+///
public interface IVideoFile
{
///
- /// The video file location id.
+ /// The video file location (VideoLocal_Place) id.
///
int ID { get; }
///
- /// The video id.
+ /// The video (VideoLocal) id.
///
int VideoID { get; }
@@ -26,12 +28,18 @@ public interface IVideoFile
string FileName { get; }
///
- /// The absolute path leading to the location of the file. Uses an OS dependent directory seperator.
+ /// The absolute path leading to the location of the file. Uses an OS dependent directory separator.
///
string Path { get; }
///
- /// The relative path from the to the location of the file. Will always use forward slash as a directory seperator.
+ /// The relative path from the to the location of the file. Will always use forward slash as a directory
+ /// separator, and will always start with a leading slash.
+ ///
+ /// E.g.
+ /// "C:\absolute\relative\path.ext" becomes "/relative/path.ext" if "C:\absolute" is the import folder.
+ /// or
+ /// "/absolute/relative/path.ext" becomes "/relative/path.ext" if "/absolute" is the import folder.
///
string RelativePath { get; }
@@ -44,35 +52,15 @@ public interface IVideoFile
/// Get the video tied to the video file location.
///
///
- IVideo? VideoInfo { get; }
+ IVideo Video { get; }
///
/// The import folder tied to the video file location.
///
- IImportFolder? ImportFolder { get; }
+ IImportFolder ImportFolder { get; }
- #region To-be-removed
-
- [Obsolete("Use VideoID instead.")]
- int VideoFileID { get; }
-
- [Obsolete("Use FileName instead. Change the 'n' to a 'N' and you're good mate.")]
- string Filename { get; }
-
- [Obsolete("Use Path instead.")]
- string FilePath { get; }
-
- [Obsolete("Use Size instead.")]
- long FileSize { get; }
-
- [Obsolete("Use VideoInfo?.Hashes instead.")]
- IHashes? Hashes { get; }
-
- [Obsolete("Use VideoInfo?.MediaInfo instead")]
- IMediaContainer? MediaInfo { get; }
-
- [Obsolete("Use VideoInfo?.AniDB instead.")]
- IAniDBFile? AniDBFileInfo { get; }
-
- #endregion
+ ///
+ /// Get the stream for the video file, if the file is still available.
+ ///
+ Stream? GetStream();
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IWithDescriptions.cs b/Shoko.Plugin.Abstractions/DataModels/IWithDescriptions.cs
index 18407355b..7527fd237 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IWithDescriptions.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IWithDescriptions.cs
@@ -2,6 +2,9 @@
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Container object with descriptions.
+///
public interface IWithDescriptions
{
///
diff --git a/Shoko.Plugin.Abstractions/DataModels/IWithImages.cs b/Shoko.Plugin.Abstractions/DataModels/IWithImages.cs
new file mode 100644
index 000000000..31b8b3fb7
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/IWithImages.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Shoko.Plugin.Abstractions.Enums;
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Container object with images.
+///
+public interface IWithImages
+{
+ ///
+ /// Get the preferred image for the given for
+ /// the entity, or null if no preferred image is found.
+ ///
+ /// The entity type to search for.
+ /// The preferred image metadata for the given entity, or null if
+ /// not found.
+ IImageMetadata? GetPreferredImageForType(ImageEntityType entityType);
+
+ ///
+ /// Get all images for the entity, or all images for the given
+ /// provided for the entity.
+ ///
+ /// If set, will restrict the returned list to only
+ /// containing the images of the given entity type.
+ /// A read-only list of images that are linked to the entity.
+ ///
+ IReadOnlyList GetImages(ImageEntityType? entityType = null);
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/IWithTitles.cs b/Shoko.Plugin.Abstractions/DataModels/IWithTitles.cs
index 76bef1667..986e4cd86 100644
--- a/Shoko.Plugin.Abstractions/DataModels/IWithTitles.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/IWithTitles.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Container object with titles.
+///
public interface IWithTitles
{
///
diff --git a/Shoko.Plugin.Abstractions/DataModels/RelationType.cs b/Shoko.Plugin.Abstractions/DataModels/RelationType.cs
index f2d8cccc0..0437e4808 100644
--- a/Shoko.Plugin.Abstractions/DataModels/RelationType.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/RelationType.cs
@@ -1,5 +1,4 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.DataModels;
///
diff --git a/Shoko.Plugin.Abstractions/DataModels/RenameScriptImpl.cs b/Shoko.Plugin.Abstractions/DataModels/RenameScriptImpl.cs
deleted file mode 100644
index d89570ca9..000000000
--- a/Shoko.Plugin.Abstractions/DataModels/RenameScriptImpl.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Shoko.Plugin.Abstractions.DataModels
-{
- public class RenameScriptImpl : IRenameScript
- {
- public string Script { get; set; }
-
- public string Type { get; set; }
-
- public string ExtraData { get; set; }
- }
-}
diff --git a/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoEpisode.cs b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoEpisode.cs
new file mode 100644
index 000000000..e944fa62b
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoEpisode.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace Shoko.Plugin.Abstractions.DataModels.Shoko;
+
+///
+/// Shoko episode metadata.
+///
+public interface IShokoEpisode : IEpisode
+{
+ ///
+ /// The id of the anidb episode linked to the shoko episode.
+ ///
+ int AnidbEpisodeID { get; }
+
+ ///
+ /// Get the shoko series info for the episode, if available.
+ ///
+ new IShokoSeries? Series { get; }
+
+ ///
+ /// A direct link to the anidb episode metadata.
+ ///
+ IEpisode AnidbEpisode { get; }
+
+ ///
+ /// All episodes linked to this shoko episode.
+ ///
+ IReadOnlyList LinkedEpisodes { get; }
+
+ ///
+ /// All movies linked to this shoko episode.
+ ///
+ IReadOnlyList LinkedMovies { get; }
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoGroup.cs b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoGroup.cs
index 37b5fb379..5478ae962 100644
--- a/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoGroup.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoGroup.cs
@@ -3,6 +3,9 @@
namespace Shoko.Plugin.Abstractions.DataModels.Shoko;
+///
+/// Shoko group metadata.
+///
public interface IShokoGroup : IWithTitles, IWithDescriptions, IMetadata
{
///
diff --git a/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoSeries.cs b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoSeries.cs
index 580e0f7d7..d09a60e63 100644
--- a/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoSeries.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoSeries.cs
@@ -1,13 +1,17 @@
+using System.Collections.Generic;
namespace Shoko.Plugin.Abstractions.DataModels.Shoko;
+///
+/// Shoko series metadata.
+///
public interface IShokoSeries : ISeries
{
///
/// AniDB anime id linked to the Shoko series.
///
int AnidbAnimeID { get; }
-
+
///
/// The id of the direct parent group of the series
///
@@ -23,6 +27,16 @@ public interface IShokoSeries : ISeries
///
ISeries AnidbAnime { get; }
+ ///
+ /// All series linked to this shoko series.
+ ///
+ IReadOnlyList LinkedSeries { get; }
+
+ ///
+ /// All movies linked to this shoko series.
+ ///
+ IReadOnlyList LinkedMovies { get; }
+
///
/// The direct parent group of the series.
///
@@ -34,4 +48,15 @@ public interface IShokoSeries : ISeries
/// structure is.
///
IShokoGroup TopLevelGroup { get; }
+
+ ///
+ /// Get an enumerable for all parent groups, starting at the
+ /// all the way up to the .
+ ///
+ IReadOnlyList AllParentGroups { get; }
+
+ ///
+ /// All episodes for the the shoko series.
+ ///
+ new IReadOnlyList Episodes { get; }
}
diff --git a/Shoko.Plugin.Abstractions/DataModels/TextDescription.cs b/Shoko.Plugin.Abstractions/DataModels/TextDescription.cs
index 932e04554..77af2c2cc 100644
--- a/Shoko.Plugin.Abstractions/DataModels/TextDescription.cs
+++ b/Shoko.Plugin.Abstractions/DataModels/TextDescription.cs
@@ -1,15 +1,35 @@
-
using Shoko.Plugin.Abstractions.Enums;
namespace Shoko.Plugin.Abstractions.DataModels;
+///
+/// Represents a text description from a data source.
+///
public class TextDescription
{
+ ///
+ /// The source.
+ ///
public DataSourceEnum Source { get; set; }
+ ///
+ /// The language.
+ ///
public TitleLanguage Language { get; set; }
+ ///
+ /// The language code.
+ ///
public string LanguageCode { get; set; } = string.Empty;
+ ///
+ /// The country code.
+ ///
+ public string? CountryCode { get; set; }
+
+ ///
+ /// The value.
+ ///
public string Value { get; set; } = string.Empty;
}
+
diff --git a/Shoko.Plugin.Abstractions/DataModels/TitleLanguage.cs b/Shoko.Plugin.Abstractions/DataModels/TitleLanguage.cs
new file mode 100644
index 000000000..8e9d9b2fb
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/TitleLanguage.cs
@@ -0,0 +1,623 @@
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// The language of a title.
+///
+public enum TitleLanguage
+{
+ ///
+ /// Main.
+ ///
+ Main = -2,
+
+ ///
+ /// None.
+ ///
+ None = -1,
+
+ ///
+ /// Unknown.
+ ///
+ Unknown = 0,
+
+ ///
+ /// English (Any).
+ ///
+ English = 1,
+
+ ///
+ /// Japanese (Romaji / Transcription).
+ ///
+ Romaji,
+
+ ///
+ /// Japanese (Kanji).
+ ///
+ Japanese,
+
+ ///
+ /// Afrikaans.
+ ///
+ Afrikaans,
+
+ ///
+ /// Arabic.
+ ///
+ Arabic,
+
+ ///
+ /// Bangladeshi.
+ ///
+ Bangladeshi,
+
+ ///
+ /// Bulgarian.
+ ///
+ Bulgarian,
+
+ ///
+ /// French Canadian.
+ ///
+ FrenchCanadian,
+
+ ///
+ /// Czech.
+ ///
+ Czech,
+
+ ///
+ /// Danish.
+ ///
+ Danish,
+
+ ///
+ /// German.
+ ///
+ German,
+
+ ///
+ /// Greek.
+ ///
+ Greek,
+
+ ///
+ /// Spanish.
+ ///
+ Spanish,
+
+ ///
+ /// Estonian.
+ ///
+ Estonian,
+
+ ///
+ /// Finnish.
+ ///
+ Finnish,
+
+ ///
+ /// French.
+ ///
+ French,
+
+ ///
+ /// Galician.
+ ///
+ Galician,
+
+ ///
+ /// Hebrew.
+ ///
+ Hebrew,
+
+ ///
+ /// Hungarian.
+ ///
+ Hungarian,
+
+ ///
+ /// Italian.
+ ///
+ Italian,
+
+ ///
+ /// Korean.
+ ///
+ Korean,
+
+ ///
+ /// Lithuanian.
+ ///
+ Lithuanian,
+
+ ///
+ /// Mongolian.
+ ///
+ Mongolian,
+
+ ///
+ /// Malaysian.
+ ///
+ Malaysian,
+
+ ///
+ /// Dutch.
+ ///
+ Dutch,
+
+ ///
+ /// Norwegian.
+ ///
+ Norwegian,
+
+ ///
+ /// Polish.
+ ///
+ Polish,
+
+ ///
+ /// Portuguese.
+ ///
+ Portuguese,
+
+ ///
+ /// Brazilian Portuguese.
+ ///
+ BrazilianPortuguese,
+
+ ///
+ /// Romanian.
+ ///
+ Romanian,
+
+ ///
+ /// Russian.
+ ///
+ Russian,
+
+ ///
+ /// Slovak.
+ ///
+ Slovak,
+
+ ///
+ /// Slovenian.
+ ///
+ Slovenian,
+
+ ///
+ /// Serbian.
+ ///
+ Serbian,
+
+ ///
+ /// Swedish.
+ ///
+ Swedish,
+
+ ///
+ /// Thai.
+ ///
+ Thai,
+
+ ///
+ /// Turkish.
+ ///
+ Turkish,
+
+ ///
+ /// Ukrainian.
+ ///
+ Ukrainian,
+
+ ///
+ /// Vietnamese.
+ ///
+ Vietnamese,
+
+ ///
+ /// Chinese (Any).
+ ///
+ Chinese,
+
+ ///
+ /// Chinese (Simplified).
+ ///
+ ChineseSimplified,
+
+ ///
+ /// Chinese (Traditional).
+ ///
+ ChineseTraditional,
+
+ ///
+ /// Chinese (Pinyin / Transcription).
+ ///
+ Pinyin,
+
+ ///
+ /// Latin.
+ ///
+ Latin,
+
+ ///
+ /// Albanian.
+ ///
+ Albanian,
+
+ ///
+ /// Basque.
+ ///
+ Basque,
+
+ ///
+ /// Bengali.
+ ///
+ Bengali,
+
+ ///
+ /// Bosnian.
+ ///
+ Bosnian,
+
+ ///
+ /// Amharic.
+ ///
+ Amharic,
+
+ ///
+ /// Armenian.
+ ///
+ Armenian,
+
+ ///
+ /// Azerbaijani.
+ ///
+ Azerbaijani,
+
+ ///
+ /// Belarusian.
+ ///
+ Belarusian,
+
+ ///
+ /// Catalan.
+ ///
+ Catalan,
+
+ ///
+ /// Chichewa.
+ ///
+ Chichewa,
+
+ ///
+ /// Corsican.
+ ///
+ Corsican,
+
+ ///
+ /// Croatian.
+ ///
+ Croatian,
+
+ ///
+ /// Divehi.
+ ///
+ Divehi,
+
+ ///
+ /// Esperanto.
+ ///
+ Esperanto,
+
+ ///
+ /// Fijian.
+ ///
+ Fijian,
+
+ ///
+ /// Georgian.
+ ///
+ Georgian,
+
+ ///
+ /// Gujarati.
+ ///
+ Gujarati,
+
+ ///
+ /// Haitian Creole.
+ ///
+ HaitianCreole,
+
+ ///
+ /// Hausa.
+ ///
+ Hausa,
+
+ ///
+ /// Icelandic.
+ ///
+ Icelandic,
+
+ ///
+ /// Igbo.
+ ///
+ Igbo,
+
+ ///
+ /// Indonesian.
+ ///
+ Indonesian,
+
+ ///
+ /// Irish.
+ ///
+ Irish,
+
+ ///
+ /// Javanese.
+ ///
+ Javanese,
+
+ ///
+ /// Kannada.
+ ///
+ Kannada,
+
+ ///
+ /// Kazakh.
+ ///
+ Kazakh,
+
+ ///
+ /// Khmer.
+ ///
+ Khmer,
+
+ ///
+ /// Kurdish.
+ ///
+ Kurdish,
+
+ ///
+ /// Kyrgyz.
+ ///
+ Kyrgyz,
+
+ ///
+ /// Lao.
+ ///
+ Lao,
+
+ ///
+ /// Latvian.
+ ///
+ Latvian,
+
+ ///
+ /// Luxembourgish.
+ ///
+ Luxembourgish,
+
+ ///
+ /// Macedonian.
+ ///
+ Macedonian,
+
+ ///
+ /// Malagasy.
+ ///
+ Malagasy,
+
+ ///
+ /// Malayalam.
+ ///
+ Malayalam,
+
+ ///
+ /// Maltese.
+ ///
+ Maltese,
+
+ ///
+ /// Maori.
+ ///
+ Maori,
+
+ ///
+ /// Marathi.
+ ///
+ Marathi,
+
+ ///
+ /// Myanmar Burmese.
+ ///
+ MyanmarBurmese,
+
+ ///
+ /// Nepali.
+ ///
+ Nepali,
+
+ ///
+ /// Oriya.
+ ///
+ Oriya,
+
+ ///
+ /// Pashto.
+ ///
+ Pashto,
+
+ ///
+ /// Persian.
+ ///
+ Persian,
+
+ ///
+ /// Punjabi.
+ ///
+ Punjabi,
+
+ ///
+ /// Quechua.
+ ///
+ Quechua,
+
+ ///
+ /// Samoan.
+ ///
+ Samoan,
+
+ ///
+ /// Scots Gaelic.
+ ///
+ ScotsGaelic,
+
+ ///
+ /// Sesotho.
+ ///
+ Sesotho,
+
+ ///
+ /// Shona.
+ ///
+ Shona,
+
+ ///
+ /// Sindhi.
+ ///
+ Sindhi,
+
+ ///
+ /// Sinhala.
+ ///
+ Sinhala,
+
+ ///
+ /// Somali.
+ ///
+ Somali,
+
+ ///
+ /// Swahili.
+ ///
+ Swahili,
+
+ ///
+ /// Tajik.
+ ///
+ Tajik,
+
+ ///
+ /// Tamil.
+ ///
+ Tamil,
+
+ ///
+ /// Tatar.
+ ///
+ Tatar,
+
+ ///
+ /// Telugu.
+ ///
+ Telugu,
+
+ ///
+ /// Turkmen.
+ ///
+ Turkmen,
+
+ ///
+ /// Uighur.
+ ///
+ Uighur,
+
+ ///
+ /// Uzbek.
+ ///
+ Uzbek,
+
+ ///
+ /// Welsh.
+ ///
+ Welsh,
+
+ ///
+ /// Xhosa.
+ ///
+ Xhosa,
+
+ ///
+ /// Yiddish.
+ ///
+ Yiddish,
+
+ ///
+ /// Yoruba.
+ ///
+ Yoruba,
+
+ ///
+ /// Zulu.
+ ///
+ Zulu,
+
+ ///
+ /// Hindi.
+ ///
+ Hindi,
+
+ ///
+ /// Filipino.
+ ///
+ Filipino,
+
+ ///
+ /// Korean (Transcription).
+ ///
+ KoreanTranscription,
+
+ ///
+ /// Thai (Transcription).
+ ///
+ ThaiTranscription,
+
+ ///
+ /// Urdu.
+ ///
+ Urdu,
+
+ ///
+ /// English (American).
+ ///
+ EnglishAmerican,
+
+ ///
+ /// English (British).
+ ///
+ EnglishBritish,
+
+ ///
+ /// English (Australian).
+ ///
+ EnglishAustralian,
+
+ ///
+ /// English (Canadian).
+ ///
+ EnglishCanadian,
+
+ ///
+ /// English (India).
+ ///
+ EnglishIndia,
+
+ ///
+ /// English (New Zealand).
+ ///
+ EnglishNewZealand,
+}
diff --git a/Shoko.Plugin.Abstractions/DataModels/TitleType.cs b/Shoko.Plugin.Abstractions/DataModels/TitleType.cs
new file mode 100644
index 000000000..9e87fba03
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/DataModels/TitleType.cs
@@ -0,0 +1,52 @@
+using System.Xml.Serialization;
+
+namespace Shoko.Plugin.Abstractions.DataModels;
+
+///
+/// Represents the type of a title.
+///
+public enum TitleType
+{
+ ///
+ /// No type specified.
+ ///
+ [XmlEnum("none")]
+ None = 0,
+
+ ///
+ /// Main title.
+ ///
+ [XmlEnum("main")]
+ Main = 1,
+
+ ///
+ /// Official title.
+ ///
+ [XmlEnum("official")]
+ Official = 2,
+
+ ///
+ /// Short title.
+ ///
+ [XmlEnum("short")]
+ Short = 3,
+
+ ///
+ /// Synonym title.
+ ///
+ [XmlEnum("syn")]
+ Synonym = 4,
+
+ ///
+ /// Title card.
+ ///
+ [XmlEnum("card")]
+ TitleCard = 5,
+
+ ///
+ /// Kana reading of the kanji title.
+ ///
+ [XmlEnum("kana")]
+ KanjiReading = 6,
+}
+
diff --git a/Shoko.Plugin.Abstractions/Enums/CodeLanguage.cs b/Shoko.Plugin.Abstractions/Enums/CodeLanguage.cs
new file mode 100644
index 000000000..1e91246c9
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Enums/CodeLanguage.cs
@@ -0,0 +1,62 @@
+namespace Shoko.Plugin.Abstractions.Enums;
+
+///
+/// Coding languages for .
+///
+public enum CodeLanguage
+{
+ ///
+ /// Plain text.
+ ///
+ PlainText = 0,
+
+ ///
+ /// C#.
+ ///
+ CSharp = 1,
+
+ ///
+ /// Java.
+ ///
+ Java = 2,
+
+ ///
+ /// JavaScript.
+ ///
+ JavaScript = 3,
+
+ ///
+ /// TypeScript.
+ ///
+ TypeScript = 4,
+
+ ///
+ /// Lua.
+ ///
+ Lua = 5,
+
+ ///
+ /// Python.
+ ///
+ Python = 6,
+
+ ///
+ /// INI.
+ ///
+ Ini = 7,
+
+ ///
+ /// JSON.
+ ///
+ Json = 8,
+
+ ///
+ /// YAML.
+ ///
+ Yaml = 9,
+
+ ///
+ /// XML.
+ ///
+ Xml = 10,
+}
diff --git a/Shoko.Plugin.Abstractions/Enums/DataSourceEnum.cs b/Shoko.Plugin.Abstractions/Enums/DataSourceEnum.cs
index 5bfb92136..9abb5ae4c 100644
--- a/Shoko.Plugin.Abstractions/Enums/DataSourceEnum.cs
+++ b/Shoko.Plugin.Abstractions/Enums/DataSourceEnum.cs
@@ -1,18 +1,48 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.Enums;
///
-/// Just a list of possible data sources. Not all are going to be used...probably
+/// Data sources.
///
public enum DataSourceEnum
{
+ ///
+ /// User (Manual).
+ ///
User = -2,
+
+ ///
+ /// Shoko.
+ ///
Shoko = -1,
+
+ ///
+ /// AniDB.
+ ///
AniDB = 0,
- MovieDB = 1,
+
+ ///
+ /// The Movie DataBase (TMDB).
+ ///
+ TMDB = 1,
+
+ ///
+ /// The Tv DataBase (TvDB).
+ ///
TvDB = 2,
+
+ ///
+ /// AniList.
+ ///
AniList = 3,
+
+ ///
+ /// Animeshon.
+ ///
Animeshon = 4,
+
+ ///
+ /// TraktTv.
+ ///
Trakt = 5,
}
diff --git a/Shoko.Plugin.Abstractions/Enums/ImageEntityType.cs b/Shoko.Plugin.Abstractions/Enums/ImageEntityType.cs
new file mode 100644
index 000000000..715babc26
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Enums/ImageEntityType.cs
@@ -0,0 +1,58 @@
+
+namespace Shoko.Plugin.Abstractions.Enums;
+
+///
+/// Image entity types.
+///
+public enum ImageEntityType
+{
+ ///
+ /// No image type.
+ ///
+ None = 0,
+
+ ///
+ /// Backdrop image.
+ ///
+ Backdrop = 1,
+
+ ///
+ /// Banner image.
+ ///
+ Banner = 2,
+
+ ///
+ /// Logo image.
+ ///
+ Logo = 3,
+
+ ///
+ /// Art image.
+ ///
+ Art = 4,
+
+ ///
+ /// Disc image.
+ ///
+ Disc = 5,
+
+ ///
+ /// Poster image.
+ ///
+ Poster = 6,
+
+ ///
+ /// Thumbnail image.
+ ///
+ Thumbnail = 7,
+
+ ///
+ /// Person image.
+ ///
+ Person = 8,
+
+ ///
+ /// Character image.
+ ///
+ Character = 9,
+}
diff --git a/Shoko.Plugin.Abstractions/Enums/NetworkAvailibility.cs b/Shoko.Plugin.Abstractions/Enums/NetworkAvailibility.cs
index 596050e10..8547d9fd8 100644
--- a/Shoko.Plugin.Abstractions/Enums/NetworkAvailibility.cs
+++ b/Shoko.Plugin.Abstractions/Enums/NetworkAvailibility.cs
@@ -1,31 +1,33 @@
-namespace Shoko.Plugin.Abstractions.Enums
+namespace Shoko.Plugin.Abstractions.Enums;
+
+///
+/// Network availability.
+///
+public enum NetworkAvailability
{
- public enum NetworkAvailability
- {
- ///
- /// Shoko was unable to find any network interfaces.
- ///
- NoInterfaces = 0,
+ ///
+ /// We were unable to find any network interfaces.
+ ///
+ NoInterfaces = 0,
- ///
- /// Shoko was unable to find any local gateways to use.
- ///
- NoGateways,
+ ///
+ /// We were unable to find any local gateways to use.
+ ///
+ NoGateways,
- ///
- /// Shoko was able to find a local gateway.
- ///
- LocalOnly,
+ ///
+ /// We were able to find a local gateway.
+ ///
+ LocalOnly,
- ///
- /// Shoko was able to connect to some internet endpoints in WAN.
- ///
- PartialInternet,
+ ///
+ /// We were able to connect to some internet endpoints in WAN.
+ ///
+ PartialInternet,
- ///
- /// Shoko was able to connect to all internet endpoints in WAN.
- ///
- Internet,
- }
+ ///
+ /// We were able to connect to all internet endpoints in WAN.
+ ///
+ Internet,
}
diff --git a/Shoko.Plugin.Abstractions/Enums/RenamerSettingType.cs b/Shoko.Plugin.Abstractions/Enums/RenamerSettingType.cs
new file mode 100644
index 000000000..57fbb5997
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Enums/RenamerSettingType.cs
@@ -0,0 +1,44 @@
+namespace Shoko.Plugin.Abstractions.Enums;
+
+///
+/// Types of settings that can be used on the settings object of a
+/// .
+///
+public enum RenamerSettingType
+{
+ ///
+ /// Auto is a special setting where it figures out what type of setting it
+ /// is using type reflection.
+ ///
+ Auto = 0,
+
+ ///
+ /// A code setting is a setting that requires a special code to be executed.
+ ///
+ Code = 1,
+
+ ///
+ /// A text setting is a setting that contains a string of text.
+ ///
+ Text = 2,
+
+ ///
+ /// A large text setting is a setting that contains a multi-line string of text.
+ ///
+ LargeText = 3,
+
+ ///
+ /// An integer setting is a setting that contains a number.
+ ///
+ Integer = 4,
+
+ ///
+ /// A decimal setting is a setting that contains a decimal number.
+ ///
+ Decimal = 5,
+
+ ///
+ /// A boolean setting is a setting that contains a true or false value.
+ ///
+ Boolean = 6,
+}
diff --git a/Shoko.Plugin.Abstractions/Enums/UpdateReason.cs b/Shoko.Plugin.Abstractions/Enums/UpdateReason.cs
index 7b31c5aab..a5d84cf6c 100644
--- a/Shoko.Plugin.Abstractions/Enums/UpdateReason.cs
+++ b/Shoko.Plugin.Abstractions/Enums/UpdateReason.cs
@@ -1,11 +1,28 @@
-#nullable enable
namespace Shoko.Plugin.Abstractions.Enums;
+///
+/// Reason for an metadata update event to be dispatched.
+///
public enum UpdateReason
{
+ ///
+ /// Nothing occirred.
+ ///
None = 0,
+
+ ///
+ /// The metadata was added.
+ ///
Added = 1,
+
+ ///
+ /// The metadata was updated.
+ ///
Updated = 2,
+
+ ///
+ /// The metadata was removed.
+ ///
Removed = 4,
}
diff --git a/Shoko.Plugin.Abstractions/Events/AVDumpEventArgs.cs b/Shoko.Plugin.Abstractions/Events/AVDumpEventArgs.cs
index f9a2ae6ba..8faad2df0 100644
--- a/Shoko.Plugin.Abstractions/Events/AVDumpEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/AVDumpEventArgs.cs
@@ -1,10 +1,11 @@
-
using System;
using System.Collections.Generic;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// AVDump event.
+///
public class AVDumpEventArgs : EventArgs
{
///
@@ -83,12 +84,22 @@ public class AVDumpEventArgs : EventArgs
///
public DateTime? EndedAt { get; set; }
+ ///
+ /// Create a new AVDump event.
+ ///
+ /// The type of event.
+ /// The message.
public AVDumpEventArgs(AVDumpEventType messageType, string? message = null)
{
Type = messageType;
Message = message;
}
+ ///
+ /// Create a new AVDump event.
+ ///
+ /// The type of event.
+ /// The exception.
public AVDumpEventArgs(AVDumpEventType messageType, Exception ex)
{
Type = messageType;
@@ -96,6 +107,9 @@ public AVDumpEventArgs(AVDumpEventType messageType, Exception ex)
}
}
+///
+/// The type of AVDump event.
+///
public enum AVDumpEventType
{
///
diff --git a/Shoko.Plugin.Abstractions/Events/AniDBBannedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/AniDBBannedEventArgs.cs
index 7f28fa59b..6a546a34e 100644
--- a/Shoko.Plugin.Abstractions/Events/AniDBBannedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/AniDBBannedEventArgs.cs
@@ -1,8 +1,10 @@
using System;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when an AniDB ban is detected.
+///
public class AniDBBannedEventArgs : EventArgs
{
///
@@ -19,6 +21,12 @@ public class AniDBBannedEventArgs : EventArgs
///
public DateTime ResumeTime { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type.
+ /// The time the ban occurred.
+ /// The resume time.
public AniDBBannedEventArgs(AniDBBanType type, DateTime time, DateTime resumeTime)
{
Type = type;
@@ -27,8 +35,18 @@ public AniDBBannedEventArgs(AniDBBanType type, DateTime time, DateTime resumeTim
}
}
+///
+/// Represents the type of AniDB ban.
+///
public enum AniDBBanType
{
+ ///
+ /// UDP ban.
+ ///
UDP,
+
+ ///
+ /// HTTP ban.
+ ///
HTTP,
}
diff --git a/Shoko.Plugin.Abstractions/Events/EpisodeInfoUpdatedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/EpisodeInfoUpdatedEventArgs.cs
index 943eafacb..c6052b2fa 100644
--- a/Shoko.Plugin.Abstractions/Events/EpisodeInfoUpdatedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/EpisodeInfoUpdatedEventArgs.cs
@@ -2,12 +2,14 @@
using Shoko.Plugin.Abstractions.DataModels;
using Shoko.Plugin.Abstractions.Enums;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
///
-/// Currently, these will fire a lot in succession, as these are updated in batch with a series.
+/// Dispatched when episode data was updated.
///
+///
+/// Currently, these will fire a lot in succession, as these are updated in batch with a series.
+///
public class EpisodeInfoUpdatedEventArgs : EventArgs
{
///
@@ -22,10 +24,16 @@ public class EpisodeInfoUpdatedEventArgs : EventArgs
///
/// This is the full data. A diff was not performed for this.
- /// This is provided for convenience, use
+ /// This is provided for convenience, use
///
public ISeries SeriesInfo { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The series info.
+ /// The episode info.
+ /// The reason it was updated.
public EpisodeInfoUpdatedEventArgs(ISeries seriesInfo, IEpisode episodeInfo, UpdateReason reason)
{
Reason = reason;
diff --git a/Shoko.Plugin.Abstractions/Events/FileDeletedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileDeletedEventArgs.cs
deleted file mode 100644
index 16d6a004c..000000000
--- a/Shoko.Plugin.Abstractions/Events/FileDeletedEventArgs.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Collections.Generic;
-using Shoko.Plugin.Abstractions.DataModels;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
-
-public class FileDeletedEventArgs : FileEventArgs
-{
- public FileDeletedEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- : base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo) { }
-}
diff --git a/Shoko.Plugin.Abstractions/Events/FileDetectedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileDetectedEventArgs.cs
index 79d3f5f58..f0411ae96 100644
--- a/Shoko.Plugin.Abstractions/Events/FileDetectedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/FileDetectedEventArgs.cs
@@ -2,18 +2,20 @@
using System.IO;
using Shoko.Plugin.Abstractions.DataModels;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when a file is detected.
+///
public class FileDetectedEventArgs : EventArgs
{
///
- /// The relative path from the 's root. Uses an OS dependent directory seperator.
+ /// The relative path from the 's root. Uses an OS dependent directory separator.
///
public string RelativePath { get; }
///
- /// The raw for the file. Don't go and accidentially delete the file now, okay?
+ /// The raw for the file. Don't go and accidentally delete the file now, okay?
///
public FileInfo FileInfo { get; }
@@ -22,6 +24,12 @@ public class FileDetectedEventArgs : EventArgs
///
public IImportFolder ImportFolder { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The relative path from the 's root. Uses an OS dependent directory separator.
+ /// The raw for the file. Don't go and accidentally delete the file now, okay?
+ /// The import folder that the file is in.
public FileDetectedEventArgs(string relativePath, FileInfo fileInfo, IImportFolder importFolder)
{
relativePath = relativePath
diff --git a/Shoko.Plugin.Abstractions/Events/FileEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileEventArgs.cs
index b4046f412..bfe4e0b47 100644
--- a/Shoko.Plugin.Abstractions/Events/FileEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/FileEventArgs.cs
@@ -3,15 +3,35 @@
using System.IO;
using System.Linq;
using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when a file event is needed. This is shared across a few events,
+/// and also the base class for more specific events.
+///
public class FileEventArgs : EventArgs
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The import folder that the file is in.
+ /// The raw for the file.
+ /// The video information for the file.
+ ///
+ /// This constructor is used to create an instance of when the relative path of the file is not known.
+ ///
public FileEventArgs(IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo)
: this(fileInfo.Path.Substring(importFolder.Path.Length), importFolder, fileInfo, videoInfo) { }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Relative path to the file.
+ /// Import folder.
+ /// File info.
+ /// Video info.
public FileEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo)
{
relativePath = relativePath
@@ -21,19 +41,40 @@ public FileEventArgs(string relativePath, IImportFolder importFolder, IVideoFile
relativePath = Path.DirectorySeparatorChar + relativePath;
RelativePath = relativePath;
ImportFolder = importFolder;
- FileInfo = fileInfo;
- VideoInfo = videoInfo;
- EpisodeInfo = VideoInfo.EpisodeInfo;
- AnimeInfo = VideoInfo.SeriesInfo
- .OfType()
- .ToArray();
- GroupInfo = VideoInfo.GroupInfo;
+ File = fileInfo;
+ Video = videoInfo;
+ Episodes = Video.Episodes;
+ Series = Video.Series;
+ Groups = Video.Groups;
}
- public FileEventArgs(IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The import folder that the file is in.
+ /// The raw for the file.
+ /// The info for the file.
+ /// The collection of info for the file.
+ /// The collection of info for the file.
+ /// The collection of info for the file.
+ ///
+ /// This constructor is intended to be used when the relative path is not known.
+ /// It is recommended to use the other constructor whenever possible.
+ ///
+ public FileEventArgs(IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
: this(fileInfo.Path.Substring(importFolder.Path.Length), importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo) { }
- public FileEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Relative path to the file.
+ /// Import folder.
+ /// File info.
+ /// Video info.
+ /// Episode info.
+ /// Series info.
+ /// /// Group info.
+ public FileEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
{
relativePath = relativePath
.Replace('/', Path.DirectorySeparatorChar)
@@ -42,45 +83,46 @@ public FileEventArgs(string relativePath, IImportFolder importFolder, IVideoFile
relativePath = Path.DirectorySeparatorChar + relativePath;
RelativePath = relativePath;
ImportFolder = importFolder;
- FileInfo = fileInfo;
- VideoInfo = videoInfo;
- EpisodeInfo = episodeInfo.ToArray();
- AnimeInfo = animeInfo.ToArray();
- GroupInfo = groupInfo.ToArray();
+ File = fileInfo;
+ Video = videoInfo;
+ Episodes = episodeInfo.ToArray();
+ Series = animeInfo.ToArray();
+ Groups = groupInfo.ToArray();
}
///
- /// The relative path from the 's root. Uses an OS dependent directory seperator.
+ /// The relative path from the 's root.
+ /// Uses an OS dependent directory separator.
///
public string RelativePath { get; }
///
- /// Information about the video and video file location, such as ids, media info, hashes, etc..
+ /// The video file location.
///
- public IVideoFile FileInfo { get; }
+ public IVideoFile File { get; }
///
- /// Information about the video.
+ /// The video.
///
- public IVideo VideoInfo { get; }
+ public IVideo Video { get; }
///
- /// The import folder that the file is in.
+ /// The import folder that the file is located in.
///
public IImportFolder ImportFolder { get; }
///
- /// Episodes Linked to the file.
+ /// Episodes linked to the video.
///
- public IReadOnlyList EpisodeInfo { get; }
+ public IReadOnlyList Episodes { get; }
///
- /// Information about the Anime, such as titles
+ /// Series linked to the video.
///
- public IReadOnlyList AnimeInfo { get; }
+ public IReadOnlyList Series { get; }
///
- /// Information about the group
+ /// Groups linked to the series that are in turn linked to the video.
///
- public IReadOnlyList GroupInfo { get; }
+ public IReadOnlyList Groups { get; }
}
diff --git a/Shoko.Plugin.Abstractions/Events/FileHashedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileHashedEventArgs.cs
deleted file mode 100644
index dcb0b215b..000000000
--- a/Shoko.Plugin.Abstractions/Events/FileHashedEventArgs.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Collections.Generic;
-using Shoko.Plugin.Abstractions.DataModels;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
-
-public class FileHashedEventArgs : FileEventArgs
-{
- public FileHashedEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- : base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo) { }
-}
diff --git a/Shoko.Plugin.Abstractions/Events/FileMatchedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileMatchedEventArgs.cs
deleted file mode 100644
index a1e8d9d0d..000000000
--- a/Shoko.Plugin.Abstractions/Events/FileMatchedEventArgs.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-using Shoko.Plugin.Abstractions.DataModels;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
-
-public class FileMatchedEventArgs : FileEventArgs
-{
- public FileMatchedEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- : base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo) { }
-}
-
diff --git a/Shoko.Plugin.Abstractions/Events/FileMovedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileMovedEventArgs.cs
index ea887f0d4..b145680eb 100644
--- a/Shoko.Plugin.Abstractions/Events/FileMovedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/FileMovedEventArgs.cs
@@ -2,10 +2,13 @@
using System.Collections.Generic;
using System.IO;
using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when a file is moved.
+///
public class FileMovedEventArgs : FileEventArgs
{
///
@@ -19,23 +22,19 @@ public class FileMovedEventArgs : FileEventArgs
///
public IImportFolder PreviousImportFolder { get; set; }
- #region To-be-removed
-
- [Obsolete("Use ImportFolder instead.")]
- public IImportFolder NewImportFolder => ImportFolder;
-
- [Obsolete("Use RelativePath instead.")]
- public string NewRelativePath => RelativePath;
-
- [Obsolete("Use PreviousImportFolder instead.")]
- public IImportFolder OldImportFolder => PreviousImportFolder;
-
- [Obsolete("Use PreviousRelativePath instead.")]
- public string OldRelativePath => PreviousRelativePath;
-
- #endregion
-
- public FileMovedEventArgs(string relativePath, IImportFolder importFolder, string previousRelativePath, IImportFolder previousImportFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Relative path to the file.
+ /// Import folder.
+ /// Previous relative path to the file from the 's base location.
+ /// Previous import folder that the file was in.
+ /// File info.
+ /// Video info.
+ /// Episode info.
+ /// Series info.
+ /// Group info.
+ public FileMovedEventArgs(string relativePath, IImportFolder importFolder, string previousRelativePath, IImportFolder previousImportFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
: base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo)
{
previousRelativePath = previousRelativePath
diff --git a/Shoko.Plugin.Abstractions/Events/FileNotMatchedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileNotMatchedEventArgs.cs
index 7ae134703..d0ebd1f20 100644
--- a/Shoko.Plugin.Abstractions/Events/FileNotMatchedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/FileNotMatchedEventArgs.cs
@@ -1,9 +1,16 @@
using System.Collections.Generic;
using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when a file is not matched, be it when it's the first check and
+/// there isn't a match yet, or for existing matches if it didn't change from
+/// the last check, or if we're UDP banned. Look at
+/// , and/or
+/// for more info.
+///
public class FileNotMatchedEventArgs : FileEventArgs
{
///
@@ -12,7 +19,7 @@ public class FileNotMatchedEventArgs : FileEventArgs
public int AutoMatchAttempts { get; }
///
- /// True if this file had existing cross-refernces before this match
+ /// True if this file had existing cross-references before this match
/// attempt.
///
public bool HasCrossReferences { get; }
@@ -22,7 +29,20 @@ public class FileNotMatchedEventArgs : FileEventArgs
///
public bool IsUDPBanned { get; }
- public FileNotMatchedEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo, int autoMatchAttempts, bool hasCrossReferences, bool isUdpBanned)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Relative path to the file.
+ /// Import folder.
+ /// File info.
+ /// Video info.
+ /// Episode info.
+ /// Series info.
+ /// Group info.
+ /// Number of times we've tried to auto-match this file up until now.
+ /// True if this file had existing cross-references before this match attempt.
+ /// True if we're currently UDP banned.
+ public FileNotMatchedEventArgs(string relativePath, IImportFolder importFolder, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo, int autoMatchAttempts, bool hasCrossReferences, bool isUdpBanned)
: base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo)
{
AutoMatchAttempts = autoMatchAttempts;
diff --git a/Shoko.Plugin.Abstractions/Events/FileRenamedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/FileRenamedEventArgs.cs
index 330fd9348..2943af7e1 100644
--- a/Shoko.Plugin.Abstractions/Events/FileRenamedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/FileRenamedEventArgs.cs
@@ -1,16 +1,18 @@
-
using System;
using System.Collections.Generic;
using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when a file is renamed.
+///
public class FileRenamedEventArgs : FileEventArgs
{
///
/// The previous relative path of the file from the
- /// 's base location.
+ /// 's base location.
///
public string PreviousRelativePath =>
RelativePath.Substring(0, RelativePath.Length - FileName.Length) + PreviousFileName;
@@ -25,18 +27,20 @@ public class FileRenamedEventArgs : FileEventArgs
///
public string PreviousFileName { get; }
- #region To-be-removed
-
- [Obsolete("Use FileName instead.")]
- public string NewFileName => FileName;
-
- [Obsolete("Use PreviousFileName instead.")]
- public string OldFileName => PreviousFileName;
-
- #endregion
-
- public FileRenamedEventArgs(string relativePath, IImportFolder importFolder, string fileName, string previousFileName, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- : base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, animeInfo, groupInfo)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Relative path to the file.
+ /// Import folder.
+ /// New file name.
+ /// Previous file name.
+ /// File info.
+ /// Video info.
+ /// Episode info.
+ /// Series info.
+ /// Group info.
+ public FileRenamedEventArgs(string relativePath, IImportFolder importFolder, string fileName, string previousFileName, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable seriesInfo, IEnumerable groupInfo)
+ : base(relativePath, importFolder, fileInfo, videoInfo, episodeInfo, seriesInfo, groupInfo)
{
FileName = fileName;
PreviousFileName = previousFileName;
diff --git a/Shoko.Plugin.Abstractions/Events/MoveEventArgs.cs b/Shoko.Plugin.Abstractions/Events/MoveEventArgs.cs
deleted file mode 100644
index 7f4da3f86..000000000
--- a/Shoko.Plugin.Abstractions/Events/MoveEventArgs.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using Shoko.Plugin.Abstractions.DataModels;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
-
-///
-/// The event arguments for a Move event. It can be cancelled by setting Cancel to true or skipped by not setting the result parameters.
-///
-public class MoveEventArgs : CancelEventArgs
-{
- ///
- /// The renamer script contents
- ///
- public IRenameScript Script { get; }
-
- ///
- /// The available import folders to choose as a destination. You can set the to one of these.
- /// If a Folder has set, then it won't be in this list.
- ///
- public IReadOnlyList AvailableFolders { get; }
-
- ///
- /// Information about the file itself, such as MediaInfo
- ///
- public IVideoFile FileInfo { get; }
-
- ///
- /// Information about the video.
- ///
- public IVideo VideoInfo { get; }
-
- ///
- /// Information about the episode, such as titles
- ///
- public IReadOnlyList EpisodeInfo { get; }
-
- ///
- /// Information about the Anime, such as titles
- ///
- public IReadOnlyList AnimeInfo { get; }
-
- ///
- /// Information about the group
- ///
- public IReadOnlyList GroupInfo { get; }
-
- public MoveEventArgs(IRenameScript script, IEnumerable availableFolders, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- {
- Script = script;
- AvailableFolders = availableFolders.ToArray();
- FileInfo = fileInfo;
- VideoInfo = videoInfo;
- EpisodeInfo = episodeInfo.ToArray();
- AnimeInfo = animeInfo.ToArray();
- GroupInfo = groupInfo.ToArray();
- }
-}
-
diff --git a/Shoko.Plugin.Abstractions/Events/MovieInfoUpdatedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/MovieInfoUpdatedEventArgs.cs
new file mode 100644
index 000000000..b54d280c6
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Events/MovieInfoUpdatedEventArgs.cs
@@ -0,0 +1,32 @@
+using System;
+using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.Enums;
+
+namespace Shoko.Plugin.Abstractions.Events;
+
+///
+/// Dispatched when movie data was updated.
+///
+public class MovieInfoUpdatedEventArgs : EventArgs
+{
+ ///
+ /// The reason this updated event was dispatched.
+ ///
+ public UpdateReason Reason { get; private set; }
+
+ ///
+ /// This is the full data. A diff was not performed for this.
+ ///
+ public IMovie MovieInfo { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The movie info.
+ /// The reason it was updated.
+ public MovieInfoUpdatedEventArgs(IMovie movieInfo, UpdateReason reason)
+ {
+ Reason = reason;
+ MovieInfo = movieInfo;
+ }
+}
diff --git a/Shoko.Plugin.Abstractions/Events/NetworkAvailabilityChangedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/NetworkAvailabilityChangedEventArgs.cs
index 2240d97c2..73c9f8fa0 100644
--- a/Shoko.Plugin.Abstractions/Events/NetworkAvailabilityChangedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/NetworkAvailabilityChangedEventArgs.cs
@@ -1,21 +1,28 @@
using System;
using Shoko.Plugin.Abstractions.Enums;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when the network availability changes.
+///
public class NetworkAvailabilityChangedEventArgs : EventArgs
{
///
- /// The new network availibility.
+ /// The new network availability.
///
- public NetworkAvailability NetworkAvailability { get; }
+ public NetworkAvailability NetworkAvailability { get; private set; }
///
/// When the last network change was detected.
///
- public DateTime LastCheckedAt { get; }
+ public DateTime LastCheckedAt { get; private set; }
+ ///
+ /// Creates a new .
+ ///
+ /// The new network availability.
+ /// When the last network change was detected.
public NetworkAvailabilityChangedEventArgs(NetworkAvailability networkAvailability, DateTime lastCheckedAt)
{
NetworkAvailability = networkAvailability;
diff --git a/Shoko.Plugin.Abstractions/Events/RelocationError.cs b/Shoko.Plugin.Abstractions/Events/RelocationError.cs
new file mode 100644
index 000000000..ad7e1e260
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Events/RelocationError.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Shoko.Plugin.Abstractions.Events;
+
+///
+/// An error or exception that occurred during a relocation operation.
+///
+public class RelocationError
+{
+ ///
+ /// The exception that caused the error, if applicable.
+ ///
+ public Exception? Exception { get; private set; }
+
+ ///
+ /// Error message. Should always be set.
+ ///
+
+ public string Message { get; private set; }
+
+ ///
+ /// Create a new relocation error.
+ ///
+ /// Error message
+ /// Exception that caused the error
+ public RelocationError(string message, Exception? exception = null)
+ {
+ Message = message;
+ Exception = exception;
+ }
+}
diff --git a/Shoko.Plugin.Abstractions/Events/RelocationEventArgs.cs b/Shoko.Plugin.Abstractions/Events/RelocationEventArgs.cs
new file mode 100644
index 000000000..6b1cc669a
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Events/RelocationEventArgs.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.DataModels.Shoko;
+
+#pragma warning disable CS8618
+namespace Shoko.Plugin.Abstractions.Events;
+
+///
+/// Event Args for File Relocation
+///
+public class RelocationEventArgs : CancelEventArgs
+{
+ ///
+ /// If the settings have moving enabled
+ ///
+ public bool MoveEnabled { get; set; }
+
+ ///
+ /// If the settings have renaming enabled
+ ///
+ public bool RenameEnabled { get; set; }
+
+ ///
+ /// The available import folders to choose as a destination. You can set the to one of these.
+ /// If a Folder has set, then it won't be in this list.
+ ///
+ public IReadOnlyList AvailableFolders { get; set; }
+
+ ///
+ /// Information about the file and video, such as MediaInfo, current location, size, etc
+ ///
+ public IVideoFile File { get; set; }
+
+ ///
+ /// Information about the episode, such as titles
+ ///
+ public IReadOnlyList Episodes { get; set; }
+
+ ///
+ /// Information about the Anime, such as titles
+ ///
+ public IReadOnlyList Series { get; set; }
+
+ ///
+ /// Information about the group
+ ///
+ public IReadOnlyList Groups { get; set; }
+}
+
+///
+/// Event Args for File Relocation with Settings
+///
+public class RelocationEventArgs : RelocationEventArgs where T : class
+{
+ ///
+ /// The settings for an
+ ///
+ public T Settings { get; set; }
+}
diff --git a/Shoko.Plugin.Abstractions/Events/RelocationResult.cs b/Shoko.Plugin.Abstractions/Events/RelocationResult.cs
new file mode 100644
index 000000000..34338298f
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Events/RelocationResult.cs
@@ -0,0 +1,68 @@
+using System.IO;
+using Shoko.Plugin.Abstractions.DataModels;
+
+namespace Shoko.Plugin.Abstractions.Events;
+
+///
+/// The result of a relocation operation by a .
+///
+public class RelocationResult
+{
+ ///
+ /// The new filename, without any path.
+ ///
+ ///
+ /// If the file name contains a path then it will be moved to
+ /// if it is unset, or otherwise discarded.
+ ///
+ /// This shouldn't be null unless a) there was an , b)
+ /// the renamer doesn't support renaming, or c) the rename operation will be
+ /// skipped as indicated by .
+ ///
+ public string? FileName { get; set; }
+
+ ///
+ /// The new path without the , relative to the import
+ /// folder.
+ ///
+ ///
+ /// Sub-folders should be separated with
+ /// or . This shouldn't be null
+ /// unless a) there was an , b) the renamer doesn't
+ /// support moving, or c) the move operation will be skipped as indicated by
+ /// .
+ ///
+ public string? Path { get; set; }
+
+ ///
+ /// The new import folder where the file should live.
+ ///
+ ///
+ /// This should be set from ,
+ /// and shouldn't be null unless a) there was an , b) the
+ /// renamer doesn't support moving, or c) the move operation will be skipped
+ /// as indicated by .
+ ///
+ public IImportFolder? DestinationImportFolder { get; set; }
+
+ ///
+ /// Indicates that the result does not contain a path and destination, and
+ /// the file should not be moved.
+ ///
+ public bool SkipMove { get; set; } = false;
+
+ ///
+ /// Indicates that the result does not contain a file name, and the file
+ /// should not be renamed.
+ ///
+ public bool SkipRename { get; set; } = false;
+
+ ///
+ /// Set this object if the event is not successful. If this is set, it
+ /// assumed that there was a failure, and the rename/move operation should
+ /// be aborted. It is required to have at least a message.
+ ///
+ /// An exception can be provided if relevant.
+ ///
+ public RelocationError? Error { get; set; }
+}
diff --git a/Shoko.Plugin.Abstractions/Events/RenameEventArgs.cs b/Shoko.Plugin.Abstractions/Events/RenameEventArgs.cs
deleted file mode 100644
index 955f87cbe..000000000
--- a/Shoko.Plugin.Abstractions/Events/RenameEventArgs.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using Shoko.Plugin.Abstractions.DataModels;
-
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
-
-public class RenameEventArgs : CancelEventArgs
-{
- ///
- /// The contents of the renamer scrpipt
- ///
- public IRenameScript Script { get; }
-
- ///
- /// The available import folders to choose as a destination. You can set the to one of these.
- /// If a Folder has set, then it won't be in this list.
- ///
- public IReadOnlyList AvailableFolders { get; }
-
- ///
- /// Information about the file itself, such as MediaInfo
- ///
- public IVideoFile FileInfo { get; }
-
- ///
- /// Information about the video.
- ///
- public IVideo VideoInfo { get; }
-
- ///
- /// Information about the episode, such as titles
- ///
- public IReadOnlyList EpisodeInfo { get; }
-
- ///
- /// Information about the Anime, such as titles
- ///
- public IReadOnlyList AnimeInfo { get; }
-
- ///
- /// Information about the group
- ///
- public IReadOnlyList GroupInfo { get; }
-
- public RenameEventArgs(IRenameScript script, IEnumerable availableFolders, IVideoFile fileInfo, IVideo videoInfo, IEnumerable episodeInfo, IEnumerable animeInfo, IEnumerable groupInfo)
- {
- Script = script;
- AvailableFolders = availableFolders.ToArray();
- FileInfo = fileInfo;
- VideoInfo = videoInfo;
- EpisodeInfo = episodeInfo.ToArray();
- AnimeInfo = animeInfo.ToArray();
- GroupInfo = groupInfo.ToArray();
- }
-}
diff --git a/Shoko.Plugin.Abstractions/Events/SeriesInfoUpdatedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/SeriesInfoUpdatedEventArgs.cs
index 155dbfc77..939e23fae 100644
--- a/Shoko.Plugin.Abstractions/Events/SeriesInfoUpdatedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/SeriesInfoUpdatedEventArgs.cs
@@ -1,28 +1,41 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using Shoko.Plugin.Abstractions.DataModels;
using Shoko.Plugin.Abstractions.Enums;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
///
-/// Fired on series info updates, currently, AniDB, TvDB, etc will trigger this
+/// Dispatched when a series metadata update occurs.
///
public class SeriesInfoUpdatedEventArgs : EventArgs
{
///
/// The reason this updated event was dispatched.
///
- public UpdateReason Reason { get; }
+ public UpdateReason Reason { get; private set; }
///
/// Anime info. This is the full data. A diff was not performed for this
///
- public ISeries SeriesInfo { get; }
+ public ISeries SeriesInfo { get; private set; }
- public SeriesInfoUpdatedEventArgs(ISeries seriesInfo, UpdateReason reason)
+ ///
+ /// The episodes that were added/updated/removed during this event.
+ ///
+ public IReadOnlyList Episodes { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The series info.
+ /// The reason it was updated.
+ /// The episodes that were added/updated/removed.
+ public SeriesInfoUpdatedEventArgs(ISeries seriesInfo, UpdateReason reason, IEnumerable? episodes = null)
{
Reason = reason;
SeriesInfo = seriesInfo;
+ Episodes = episodes?.ToList() ?? [];
}
}
diff --git a/Shoko.Plugin.Abstractions/Events/SettingsSavedEventArgs.cs b/Shoko.Plugin.Abstractions/Events/SettingsSavedEventArgs.cs
index 03a2424e0..43b8da3b1 100644
--- a/Shoko.Plugin.Abstractions/Events/SettingsSavedEventArgs.cs
+++ b/Shoko.Plugin.Abstractions/Events/SettingsSavedEventArgs.cs
@@ -1,8 +1,10 @@
using System;
-#nullable enable
-namespace Shoko.Plugin.Abstractions;
+namespace Shoko.Plugin.Abstractions.Events;
+///
+/// Dispatched when server settings have been saved.
+///
public class SettingsSavedEventArgs : EventArgs
{
}
diff --git a/Shoko.Plugin.Abstractions/Extensions/LanguageExtensions.cs b/Shoko.Plugin.Abstractions/Extensions/LanguageExtensions.cs
index 3b4383e66..aa3400b2e 100644
--- a/Shoko.Plugin.Abstractions/Extensions/LanguageExtensions.cs
+++ b/Shoko.Plugin.Abstractions/Extensions/LanguageExtensions.cs
@@ -4,19 +4,34 @@
namespace Shoko.Plugin.Abstractions.Extensions;
+///
+/// Extensions for the enum.
+///
public static class LanguageExtensions
{
+ ///
+ /// Convert a language code to a .
+ ///
+ /// The language code or name.
+ /// The or if not found.
public static TitleLanguage GetTitleLanguage(this string lang)
- {
- return lang.ToUpper() switch
+ => lang?.ToUpper() switch
{
"EN" or "ENG" => TitleLanguage.English,
+ "EN-US" => TitleLanguage.EnglishAmerican,
+ "EN-GB" => TitleLanguage.EnglishBritish,
+ "EN-AU" => TitleLanguage.EnglishAustralian,
+ "EN-CA" => TitleLanguage.EnglishCanadian,
+ "EN-IN" => TitleLanguage.EnglishIndia,
+ "EN-NZ" => TitleLanguage.EnglishNewZealand,
"X-JAT" => TitleLanguage.Romaji,
"JA" or "JPN" => TitleLanguage.Japanese,
"AR" or "ARA" => TitleLanguage.Arabic,
"BD" or "BAN" => TitleLanguage.Bangladeshi,
"BG" or "BUL" => TitleLanguage.Bulgarian,
- "CA" => TitleLanguage.FrenchCanadian,
+ // CA isn't actually french canadian, but we have it mapped as such
+ // because anidb have it mapped as such.
+ "CA" or "FR-CA" => TitleLanguage.FrenchCanadian,
"CS" or "CES" or "CZ" => TitleLanguage.Czech,
"DA" or "DAN" or "DK" => TitleLanguage.Danish,
"DE" or "DEU" => TitleLanguage.German,
@@ -24,12 +39,13 @@ public static TitleLanguage GetTitleLanguage(this string lang)
"ES" or "SPA" => TitleLanguage.Spanish,
"ET" or "EST" => TitleLanguage.Estonian,
"FI" or "FIN" => TitleLanguage.Finnish,
- "FR" or "FRA" or "CA" => TitleLanguage.French,
+ "FR" or "FRA" => TitleLanguage.French,
"GL" or "GLG" => TitleLanguage.Galician,
"HE" or "HEB" or "IL" => TitleLanguage.Hebrew,
"HU" or "HUN" => TitleLanguage.Hungarian,
"IT" or "ITA" => TitleLanguage.Italian,
"KO" or "KOR" => TitleLanguage.Korean,
+ "X-KOT" => TitleLanguage.KoreanTranscription,
"LT" or "LIT" => TitleLanguage.Lithuanian,
"MN" or "MON" => TitleLanguage.Mongolian,
"MS" or "MSA" or "MY" => TitleLanguage.Malaysian,
@@ -67,11 +83,13 @@ public static TitleLanguage GetTitleLanguage(this string lang)
"HR" or "HRV" => TitleLanguage.Croatian,
"DV" or "DIV" => TitleLanguage.Divehi,
"EO" or "EPO" => TitleLanguage.Esperanto,
+ "TL" or "FIL" => TitleLanguage.Filipino,
"FJ" or "FIJ" => TitleLanguage.Fijian,
"KA" or "KAT" => TitleLanguage.Georgian,
"GU" or "GUJ" => TitleLanguage.Gujarati,
"HT" or "HAT" => TitleLanguage.HaitianCreole,
"HA" or "HAU" => TitleLanguage.Hausa,
+ "HI" or "HIN" => TitleLanguage.Hindi,
"IS" or "ISL" => TitleLanguage.Icelandic,
"IG" or "IBO" => TitleLanguage.Igbo,
"ID" or "IND" => TitleLanguage.Indonesian,
@@ -119,39 +137,73 @@ public static TitleLanguage GetTitleLanguage(this string lang)
"YI" or "YID" => TitleLanguage.Yiddish,
"YO" or "YOR" => TitleLanguage.Yoruba,
"ZU" or "ZUL" => TitleLanguage.Zulu,
- "UNK" => TitleLanguage.Unknown,
- _ => TitleLanguage.Unknown,
+ "UR" or "URD" => TitleLanguage.Urdu,
+ "GREEK (ANCIENT)" => TitleLanguage.Greek,
+ "JAVANESE" or "MALAY" or "INDONESIAN" => TitleLanguage.Malaysian,
+ "PORTUGUESE (BRAZILIAN)" => TitleLanguage.BrazilianPortuguese,
+ "THAI (TRANSCRIPTION)" => TitleLanguage.ThaiTranscription,
+ "CHINESE (SIMPLIFIED)" => TitleLanguage.ChineseSimplified,
+ "CHINESE (TRADITIONAL)" => TitleLanguage.ChineseTraditional,
+ "CHINESE (CANTONESE)" or "CHINESE (MANDARIN)" or
+ "CHINESE (UNSPECIFIED)" or "TAIWANESE" => TitleLanguage.Chinese,
+ "CHINESE (TRANSCRIPTION)" => TitleLanguage.Pinyin,
+ "JAPANESE (TRANSCRIPTION)" => TitleLanguage.Romaji,
+ "CATALAN" or "SPANISH (LATIN AMERICAN)" => TitleLanguage.Spanish,
+ "KOREAN (TRANSCRIPTION)" => TitleLanguage.KoreanTranscription,
+ "FILIPINO (TAGALOG)" => TitleLanguage.Filipino,
+ "" => TitleLanguage.None,
+ "X-MAIN" => TitleLanguage.Main,
+ null => TitleLanguage.None,
+ _ => Enum.TryParse(lang.ToLowerInvariant(), true, out var titleLanguage) ?
+ titleLanguage : TitleLanguage.Unknown,
};
- }
-
+
+ ///
+ /// Get a user friendly description of a .
+ ///
+ /// Language value.
+ /// The friendly description.
public static string GetDescription(this TitleLanguage lang)
- {
- return lang switch
+ => lang switch
{
- TitleLanguage.Romaji => "Japanese (romanji/x-jat)",
- TitleLanguage.Japanese => "Japanese (kanji)",
+ TitleLanguage.English => "English (Any)",
+ TitleLanguage.EnglishAmerican => "English (American)",
+ TitleLanguage.EnglishBritish => "English (British)",
+ TitleLanguage.EnglishAustralian => "English (Australian)",
+ TitleLanguage.EnglishCanadian => "English (Canadian)",
+ TitleLanguage.EnglishIndia => "English (India)",
+ TitleLanguage.EnglishNewZealand => "English (New Zealand)",
+ TitleLanguage.Romaji => "Japanese (Romaji / Transcription)",
+ TitleLanguage.Japanese => "Japanese (Kanji)",
TitleLanguage.Bangladeshi => "Bangladesh",
TitleLanguage.FrenchCanadian => "Canadian-French",
TitleLanguage.BrazilianPortuguese => "Brazilian Portuguese",
TitleLanguage.Chinese => "Chinese (any)",
- TitleLanguage.ChineseSimplified => "Chinese (simplified)",
- TitleLanguage.ChineseTraditional => "Chinese (traditional)",
- TitleLanguage.Pinyin => "Chinese (pinyin/x-zhn)",
+ TitleLanguage.ChineseSimplified => "Chinese (Simplified)",
+ TitleLanguage.ChineseTraditional => "Chinese (Traditional)",
+ TitleLanguage.Pinyin => "Chinese (Pinyin / Transcription)",
+ TitleLanguage.KoreanTranscription => "Korean (Transcription)",
+ TitleLanguage.ThaiTranscription => "Thai (Transcription)",
_ => lang.ToString(),
};
- }
+ ///
+ /// Get the preferred language-code for a .
+ ///
+ /// Language value.
+ /// The preferred language-code.
public static string GetString(this TitleLanguage lang)
- {
- return lang switch
+ => lang switch
{
+ TitleLanguage.Main => "x-main",
+ TitleLanguage.None => "none",
TitleLanguage.English => "en",
TitleLanguage.Romaji => "x-jat",
TitleLanguage.Japanese => "ja",
TitleLanguage.Arabic => "ar",
TitleLanguage.Bangladeshi => "bd",
TitleLanguage.Bulgarian => "bg",
- TitleLanguage.FrenchCanadian => "ca",
+ TitleLanguage.FrenchCanadian => "fr-CA",
TitleLanguage.Czech => "cz",
TitleLanguage.Danish => "da",
TitleLanguage.German => "de",
@@ -165,6 +217,7 @@ public static string GetString(this TitleLanguage lang)
TitleLanguage.Hungarian => "hu",
TitleLanguage.Italian => "it",
TitleLanguage.Korean => "ko",
+ TitleLanguage.KoreanTranscription => "x-kot",
TitleLanguage.Lithuanian => "lt",
TitleLanguage.Mongolian => "mn",
TitleLanguage.Malaysian => "ms",
@@ -172,7 +225,7 @@ public static string GetString(this TitleLanguage lang)
TitleLanguage.Norwegian => "no",
TitleLanguage.Polish => "pl",
TitleLanguage.Portuguese => "pt",
- TitleLanguage.BrazilianPortuguese => "pt-br",
+ TitleLanguage.BrazilianPortuguese => "pt-BR",
TitleLanguage.Romanian => "ro",
TitleLanguage.Russian => "ru",
TitleLanguage.Slovak => "sk",
@@ -180,6 +233,7 @@ public static string GetString(this TitleLanguage lang)
TitleLanguage.Serbian => "sr",
TitleLanguage.Swedish => "sv",
TitleLanguage.Thai => "th",
+ TitleLanguage.ThaiTranscription => "x-tha",
TitleLanguage.Turkish => "tr",
TitleLanguage.Ukrainian => "uk",
TitleLanguage.Vietnamese => "vi",
@@ -202,11 +256,13 @@ public static string GetString(this TitleLanguage lang)
TitleLanguage.Croatian => "hr",
TitleLanguage.Divehi => "dv",
TitleLanguage.Esperanto => "eo",
+ TitleLanguage.Filipino => "tl",
TitleLanguage.Fijian => "fj",
TitleLanguage.Georgian => "ka",
TitleLanguage.Gujarati => "gu",
TitleLanguage.HaitianCreole => "ht",
TitleLanguage.Hausa => "ha",
+ TitleLanguage.Hindi => "hi",
TitleLanguage.Icelandic => "is",
TitleLanguage.Igbo => "ig",
TitleLanguage.Indonesian => "id",
@@ -249,34 +305,317 @@ public static string GetString(this TitleLanguage lang)
TitleLanguage.Turkmen => "tk",
TitleLanguage.Uighur => "ug",
TitleLanguage.Uzbek => "uz",
+ TitleLanguage.Urdu => "ur",
TitleLanguage.Welsh => "cy",
TitleLanguage.Xhosa => "xh",
TitleLanguage.Yiddish => "yi",
TitleLanguage.Yoruba => "yo",
TitleLanguage.Zulu => "zu",
+ TitleLanguage.EnglishAmerican => "en-US",
+ TitleLanguage.EnglishBritish => "en-GB",
+ TitleLanguage.EnglishAustralian => "en-AU",
+ TitleLanguage.EnglishCanadian => "en-CA",
+ TitleLanguage.EnglishIndia => "en-IN",
+ TitleLanguage.EnglishNewZealand => "en-NZ",
_ => "unk",
};
- }
-
+
+ ///
+ /// Get the language and country code for a .
+ ///
+ /// Language value.
+ public static (string languageCode, string? countryCode) GetLanguageAndCountryCode(this TitleLanguage lang)
+ => lang.GetString() is { Length: 5 } l && l[2] == '-' ? (l.Substring(0, 2), l.Substring(3)) : (lang.GetString(), null);
+
+ ///
+ /// Get the text form of a title language.
+ ///
+ ///
+ ///
public static string GetString(this TitleType type)
- {
- return type.ToString().ToLowerInvariant();
- }
+ => type.ToString().ToLowerInvariant();
+ ///
+ /// Convert from a string to a .
+ ///
+ /// Name or value.
+ /// The parse or if not found.
public static TitleType GetTitleType(this string name)
- {
- foreach (var type in Enum.GetValues(typeof(TitleType)).Cast())
- {
- if (type.GetString().Equals(name.ToLowerInvariant())) return type;
- }
-
- return name.ToLowerInvariant() switch
+ => name.ToLowerInvariant() switch
{
"syn" => TitleType.Synonym,
"card" => TitleType.TitleCard,
"kana" => TitleType.KanjiReading,
"kanareading" => TitleType.KanjiReading,
- _ => TitleType.None,
+ _ => Enum.TryParse(name, true, out var type) ? type : TitleType.None,
+ };
+
+ ///
+ /// Convert from an ISO3166 Alpha-2 or Alpha-3 country code to an ISO639-1
+ /// language code.
+ ///
+ ///
+ /// This conversion list was compiled using
+ /// https://github.com/annexare/Countries as a base, since it was the most
+ /// complete library i could find that contained some kind of mapping
+ /// between countries and languages, and with some minor modifications
+ /// afterwards.
+ ///
+ /// Alpha-2 or Alpha-3 country code.
+ ///
+ public static string FromIso3166ToIso639(this string? countryCode)
+ => countryCode?.ToUpper() switch
+ {
+ "AD" or "AND" => "CA",
+ "AE" or "ARE" => "AR",
+ "AF" or "AFG" => "PS",
+ "AG" or "ATG" => "EN",
+ "AI" or "AIA" => "EN",
+ "AL" or "ALB" => "SQ",
+ "AM" or "ARM" => "HY",
+ "AO" or "AGO" => "PT",
+ "AQ" or "ATA" => "EN",
+ "AR" or "ARG" => "ES",
+ "AS" or "ASM" => "EN",
+ "AT" or "AUT" => "DE",
+ "AU" or "AUS" => "EN-AU",
+ "AW" or "ABW" => "NL",
+ "AX" or "ALA" => "SV",
+ "AZ" or "AZE" => "AZ",
+ "BA" or "BIH" => "BS",
+ "BB" or "BRB" => "EN",
+ "BD" or "BGD" => "BN",
+ "BE" or "BEL" => "NL",
+ "BF" or "BFA" => "FR",
+ "BG" or "BGR" => "BG",
+ "BH" or "BHR" => "AR",
+ "BI" or "BDI" => "FR",
+ "BJ" or "BEN" => "FR",
+ "BL" or "BLM" => "FR",
+ "BM" or "BMU" => "EN",
+ "BN" or "BRN" => "MS",
+ "BO" or "BOL" => "ES",
+ "BQ" or "BES" => "NL",
+ "BR" or "BRA" => "PT-BR",
+ "BS" or "BHS" => "EN",
+ "BT" or "BTN" => "DZ",
+ "BV" or "BVT" => "NO",
+ "BW" or "BWA" => "EN",
+ "BY" or "BLR" => "BE",
+ "BZ" or "BLZ" => "EN",
+ "CA" or "CAN" => "EN-CA",
+ "CC" or "CCK" => "EN",
+ "CD" or "COD" => "FR",
+ "CF" or "CAF" => "FR",
+ "CG" or "COG" => "FR",
+ "CH" or "CHE" => "DE",
+ "CI" or "CIV" => "FR",
+ "CK" or "COK" => "EN",
+ "CL" or "CHL" => "ES",
+ "CM" or "CMR" => "EN",
+ "CN" or "CHN" => "ZH-HANS",
+ "CO" or "COL" => "ES",
+ "CR" or "CRI" => "ES",
+ "CU" or "CUB" => "ES",
+ "CV" or "CPV" => "PT",
+ "CW" or "CUW" => "NL",
+ "CX" or "CXR" => "EN",
+ "CY" or "CYP" => "EL",
+ "CZ" or "CZE" => "CS",
+ "DE" or "DEU" => "DE",
+ "DJ" or "DJI" => "FR",
+ "DK" or "DNK" => "DA",
+ "DM" or "DMA" => "EN",
+ "DO" or "DOM" => "ES",
+ "DZ" or "DZA" => "AR",
+ "EC" or "ECU" => "ES",
+ "EE" or "EST" => "ET",
+ "EG" or "EGY" => "AR",
+ "EH" or "ESH" => "ES",
+ "ER" or "ERI" => "TI",
+ "ES" or "ESP" => "ES",
+ "ET" or "ETH" => "AM",
+ "FI" or "FIN" => "FI",
+ "FJ" or "FJI" => "EN",
+ "FK" or "FLK" => "EN",
+ "FM" or "FSM" => "EN",
+ "FO" or "FRO" => "FO",
+ "FR" or "FRA" => "FR",
+ "GA" or "GAB" => "FR",
+ "GB" or "GBR" => "EN-GB",
+ "GD" or "GRD" => "EN",
+ "GE" or "GEO" => "KA",
+ "GF" or "GUF" => "FR",
+ "GG" or "GGY" => "EN",
+ "GH" or "GHA" => "EN",
+ "GI" or "GIB" => "EN",
+ "GL" or "GRL" => "KL",
+ "GM" or "GMB" => "EN",
+ "GN" or "GIN" => "FR",
+ "GP" or "GLP" => "FR",
+ "GQ" or "GNQ" => "ES",
+ "GR" or "GRC" => "EL",
+ "GS" or "SGS" => "EN",
+ "GT" or "GTM" => "ES",
+ "GU" or "GUM" => "EN",
+ "GW" or "GNB" => "PT",
+ "GY" or "GUY" => "EN",
+ "HK" or "HKG" => "ZH-HANT",
+ "HM" or "HMD" => "EN",
+ "HN" or "HND" => "ES",
+ "HR" or "HRV" => "HR",
+ "HT" or "HTI" => "FR",
+ "HU" or "HUN" => "HU",
+ "ID" or "IDN" => "ID",
+ "IE" or "IRL" => "GA",
+ "IL" or "ISR" => "HE",
+ "IM" or "IMN" => "EN",
+ "IN" or "IND" => "EN-IN",
+ "IO" or "IOT" => "EN",
+ "IQ" or "IRQ" => "AR",
+ "IR" or "IRN" => "FA",
+ "IS" or "ISL" => "IS",
+ "IT" or "ITA" => "IT",
+ "JE" or "JEY" => "EN",
+ "JM" or "JAM" => "EN",
+ "JO" or "JOR" => "AR",
+ "JP" or "JPN" => "JA",
+ "KE" or "KEN" => "EN",
+ "KG" or "KGZ" => "KY",
+ "KH" or "KHM" => "KM",
+ "KI" or "KIR" => "EN",
+ "KM" or "COM" => "AR",
+ "KN" or "KNA" => "EN",
+ "KP" or "PRK" => "KO",
+ "KR" or "KOR" => "KO",
+ "KW" or "KWT" => "AR",
+ "KY" or "CYM" => "EN",
+ "KZ" or "KAZ" => "KK",
+ "LA" or "LAO" => "LO",
+ "LB" or "LBN" => "AR",
+ "LC" or "LCA" => "EN",
+ "LI" or "LIE" => "DE",
+ "LK" or "LKA" => "SI",
+ "LR" or "LBR" => "EN",
+ "LS" or "LSO" => "EN",
+ "LT" or "LTU" => "LT",
+ "LU" or "LUX" => "FR",
+ "LV" or "LVA" => "LV",
+ "LY" or "LBY" => "AR",
+ "MA" or "MAR" => "AR",
+ "MC" or "MCO" => "FR",
+ "MD" or "MDA" => "RO",
+ "ME" or "MNE" => "SR",
+ "MF" or "MAF" => "EN",
+ "MG" or "MDG" => "FR",
+ "MH" or "MHL" => "EN",
+ "MK" or "MKD" => "MK",
+ "ML" or "MLI" => "FR",
+ "MM" or "MMR" => "MY",
+ "MN" or "MNG" => "MN",
+ "MO" or "MAC" => "ZH",
+ "MP" or "MNP" => "EN",
+ "MQ" or "MTQ" => "FR",
+ "MR" or "MRT" => "AR",
+ "MS" or "MSR" => "EN",
+ "MT" or "MLT" => "MT",
+ "MU" or "MUS" => "EN",
+ "MV" or "MDV" => "DV",
+ "MW" or "MWI" => "EN",
+ "MX" or "MEX" => "ES",
+ "MY" or "MYS" => "MS",
+ "MZ" or "MOZ" => "PT",
+ "NA" or "NAM" => "EN",
+ "NC" or "NCL" => "FR",
+ "NE" or "NER" => "FR",
+ "NF" or "NFK" => "EN",
+ "NG" or "NGA" => "EN",
+ "NI" or "NIC" => "ES",
+ "NL" or "NLD" => "NL",
+ "NO" or "NOR" => "NO",
+ "NP" or "NPL" => "NE",
+ "NR" or "NRU" => "EN",
+ "NU" or "NIU" => "EN",
+ "NZ" or "NZL" => "EN-NZ",
+ "OM" or "OMN" => "AR",
+ "PA" or "PAN" => "ES",
+ "PE" or "PER" => "ES",
+ "PF" or "PYF" => "FR",
+ "PG" or "PNG" => "EN",
+ "PH" or "PHL" => "EN",
+ "PK" or "PAK" => "EN",
+ "PL" or "POL" => "PL",
+ "PM" or "SPM" => "FR",
+ "PN" or "PCN" => "EN",
+ "PR" or "PRI" => "ES",
+ "PS" or "PSE" => "AR",
+ "PT" or "PRT" => "PT",
+ "PW" or "PLW" => "EN",
+ "PY" or "PRY" => "ES",
+ "QA" or "QAT" => "AR",
+ "RE" or "REU" => "FR",
+ "RO" or "ROU" => "RO",
+ "RS" or "SRB" => "SR",
+ "RU" or "RUS" => "RU",
+ "RW" or "RWA" => "RW",
+ "SA" or "SAU" => "AR",
+ "SB" or "SLB" => "EN",
+ "SC" or "SYC" => "FR",
+ "SD" or "SDN" => "AR",
+ "SE" or "SWE" => "SV",
+ "SG" or "SGP" => "EN",
+ "SH" or "SHN" => "EN",
+ "SI" or "SVN" => "SL",
+ "SJ" or "SJM" => "NO",
+ "SK" or "SVK" => "SK",
+ "SL" or "SLE" => "EN",
+ "SM" or "SMR" => "IT",
+ "SN" or "SEN" => "FR",
+ "SO" or "SOM" => "SO",
+ "SR" or "SUR" => "NL",
+ "SS" or "SSD" => "EN",
+ "ST" or "STP" => "PT",
+ "SV" or "SLV" => "ES",
+ "SX" or "SXM" => "NL",
+ "SY" or "SYR" => "AR",
+ "SZ" or "SWZ" => "EN",
+ "TC" or "TCA" => "EN",
+ "TD" or "TCD" => "FR",
+ "TF" or "ATF" => "FR",
+ "TG" or "TGO" => "FR",
+ "TH" or "THA" => "TH",
+ "TJ" or "TJK" => "TG",
+ "TK" or "TKL" => "EN",
+ "TL" or "TLS" => "PT",
+ "TM" or "TKM" => "TK",
+ "TN" or "TUN" => "AR",
+ "TO" or "TON" => "EN",
+ "TR" or "TUR" => "TR",
+ "TT" or "TTO" => "EN",
+ "TV" or "TUV" => "EN",
+ "TW" or "TWN" => "ZH-HANT",
+ "TZ" or "TZA" => "SW",
+ "UA" or "UKR" => "UK",
+ "UG" or "UGA" => "EN",
+ "UM" or "UMI" => "EN",
+ "US" or "USA" => "EN-US",
+ "UY" or "URY" => "ES",
+ "UZ" or "UZB" => "UZ",
+ "VA" or "VAT" => "IT",
+ "VC" or "VCT" => "EN",
+ "VE" or "VEN" => "ES",
+ "VG" or "VGB" => "EN",
+ "VI" or "VIR" => "EN",
+ "VN" or "VNM" => "VI",
+ "VU" or "VUT" => "BI",
+ "WF" or "WLF" => "FR",
+ "WS" or "WSM" => "SM",
+ "XK" or "XKX" => "SQ",
+ "YE" or "YEM" => "AR",
+ "YT" or "MYT" => "FR",
+ "ZA" or "ZAF" => "AF",
+ "ZM" or "ZMB" => "EN",
+ "ZW" or "ZWE" => "EN",
+ _ => countryCode?.ToUpper() ?? "UNK",
};
- }
}
diff --git a/Shoko.Plugin.Abstractions/Extensions/NetworkAvailabilityExtensions.cs b/Shoko.Plugin.Abstractions/Extensions/NetworkAvailabilityExtensions.cs
index 37bad467b..03722b5a8 100644
--- a/Shoko.Plugin.Abstractions/Extensions/NetworkAvailabilityExtensions.cs
+++ b/Shoko.Plugin.Abstractions/Extensions/NetworkAvailabilityExtensions.cs
@@ -1,10 +1,17 @@
using Shoko.Plugin.Abstractions.Enums;
-namespace Shoko.Plugin.Abstractions.Extensions
+namespace Shoko.Plugin.Abstractions.Extensions;
+
+///
+/// Extensions for the enum.
+///
+public static class NetworkAvailabilityExtensions
{
- public static class NetworkAvailabilityExtensions
- {
- public static bool HasInternet(this NetworkAvailability value)
- => value is NetworkAvailability.Internet or NetworkAvailability.PartialInternet;
- }
+ ///
+ /// Returns true if the is or
+ ///
+ /// Value to check.
+ /// True if the is or .
+ public static bool HasInternet(this NetworkAvailability value)
+ => value is NetworkAvailability.Internet or NetworkAvailability.PartialInternet;
}
diff --git a/Shoko.Plugin.Abstractions/IPlugin.cs b/Shoko.Plugin.Abstractions/IPlugin.cs
index 566da9310..cb86f23ec 100644
--- a/Shoko.Plugin.Abstractions/IPlugin.cs
+++ b/Shoko.Plugin.Abstractions/IPlugin.cs
@@ -1,24 +1,27 @@
-namespace Shoko.Plugin.Abstractions
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// Interface for plugins to register themselves automagically.
+///
+public interface IPlugin
{
///
- /// This can specify the static method
- /// static void ConfigureServices(IServiceCollection serviceCollection)
- /// This will allow you to inject other services to the Shoko DI container which can be accessed via Utils.ServiceContainer
- /// if you want a Logger, you can use
+ /// Friendly name of the plugin.
+ ///
+ string Name { get; }
+
+ ///
+ /// Load event.
+ ///
+ void Load();
+
+ ///
+ /// This will be called with the created settings object if you have an in the Plugin.
+ /// You can cast to your desired type and set the settings within it.
///
- public interface IPlugin
- {
- string Name { get; }
- void Load();
-
- ///
- /// This will be called with the created settings object if you have an in the Plugin.
- /// You can cast to your desired type and set the settings within it.
- ///
- ///
- void OnSettingsLoaded(IPluginSettings settings);
+ ///
+ void OnSettingsLoaded(IPluginSettings settings);
- // static void ConfigureServices(IServiceCollection serviceCollection);
- }
-}
+ // static void ConfigureServices(IServiceCollection serviceCollection);
+}
diff --git a/Shoko.Plugin.Abstractions/IPluginSettings.cs b/Shoko.Plugin.Abstractions/IPluginSettings.cs
index 028cf5b55..fc1a4bcc3 100644
--- a/Shoko.Plugin.Abstractions/IPluginSettings.cs
+++ b/Shoko.Plugin.Abstractions/IPluginSettings.cs
@@ -1,11 +1,8 @@
-namespace Shoko.Plugin.Abstractions
-{
- ///
- /// This interface is primarily an identifier. The final model is serialized as json using the Server Settings manager.
- /// The Settings are saved in the data folder under Plugins. There can be only one! (per assembly)
- ///
- public interface IPluginSettings
- {
-
- }
-}
\ No newline at end of file
+
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// This interface is primarily an identifier. The final model is serialized as json using the Server Settings manager.
+/// The Settings are saved in the data folder under Plugins. There can be only one! (per assembly)
+///
+public interface IPluginSettings { }
diff --git a/Shoko.Plugin.Abstractions/IRenameScript.cs b/Shoko.Plugin.Abstractions/IRenameScript.cs
deleted file mode 100644
index 209de1906..000000000
--- a/Shoko.Plugin.Abstractions/IRenameScript.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Shoko.Plugin.Abstractions
-{
- public interface IRenameScript
- {
- ///
- /// The script contents
- ///
- public string Script { get; }
- ///
- /// The type of the renamer, always should be checked against the Renamer ID to ensure that the script should be executable against your renamer.
- ///
- public string Type { get; }
- ///
- /// Any extra data provided.
- ///
- public string ExtraData { get; }
- }
-}
diff --git a/Shoko.Plugin.Abstractions/IRenamer.cs b/Shoko.Plugin.Abstractions/IRenamer.cs
index 5aac656fb..c3a886725 100644
--- a/Shoko.Plugin.Abstractions/IRenamer.cs
+++ b/Shoko.Plugin.Abstractions/IRenamer.cs
@@ -1,18 +1,88 @@
-using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.Events;
namespace Shoko.Plugin.Abstractions;
+///
+/// The base interface for any renamer.
+///
+public interface IBaseRenamer
+{
+ ///
+ /// The human-readable name of the renamer
+ ///
+ public string Name { get; }
+
+ ///
+ /// The description of the renamer
+ ///
+ public string Description { get; }
+
+ ///
+ /// This should be true if the renamer supports moving.
+ /// If it is set to false, it will use an with even attempting to use the result from or
+ ///
+ bool SupportsMoving { get; }
+
+ ///
+ /// This should be true if the renamer supports renaming.
+ /// If it is set to false, it will use an with even attempting to use the result from or
+ ///
+ bool SupportsRenaming { get; }
+}
+
///
/// A renamer that knows how to operate on recognized files.
///
-public interface IRenamer
+public interface IRenamer : IBaseRenamer
+{
+ ///
+ /// Get the new path for moving and/or renaming. See and its for details on the return value.
+ ///
+ ///
+ ///
+ RelocationResult GetNewPath(RelocationEventArgs args);
+}
+///
+/// A renamer with a settings model.
+///
+/// Type of the settings model
+public interface IRenamer : IBaseRenamer where T : class
{
- string GetFilename(RenameEventArgs args);
+ ///
+ /// Allows the IRenamer to return a default settings model.
+ ///
+ T? DefaultSettings { get; }
- (IImportFolder destination, string subfolder) GetDestination(MoveEventArgs args);
+ ///
+ /// Get the new path for moving and/or renaming. See and its for details on the return value.
+ ///
+ ///
+ ///
+ RelocationResult GetNewPath(RelocationEventArgs args);
}
///
/// A renamer that knows how to operate on both recognized and unrecognized files.
///
-public interface IUnrecognizedRenamer : IRenamer { }
+public interface IUnrecognizedRenamer : IRenamer;
+
+///
+/// A renamer that knows how to operate on both recognized and unrecognized files.
+/// This version also takes in a settings model.
+///
+/// Type of the settings model
+public interface IUnrecognizedRenamer : IRenamer where T : class;
+
+///
+/// A renamer that should be used if the primary renamer does not support the operation.
+/// The Legacy/WebAOM renamer does not explicitly implement this, but will be used as a fallback if another is not provided and the primary renamer does not support the operation.
+///
+public interface IFallbackRenamer : IRenamer;
+
+///
+/// A renamer that should be used if the primary renamer does not support the operation.
+/// The Legacy/WebAOM renamer does not explicitly implement this, but will be used as a fallback if another is not provided and the primary renamer does not support the operation.
+/// This version also takes in a settings model.
+///
+/// Type of the settings model
+public interface IFallbackRenamer : IRenamer where T : class;
diff --git a/Shoko.Plugin.Abstractions/IRenamerConfig.cs b/Shoko.Plugin.Abstractions/IRenamerConfig.cs
new file mode 100644
index 000000000..96cdccddb
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/IRenamerConfig.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// Interface for a renamer configuration type.
+///
+public interface IRenamerConfig
+{
+ ///
+ /// The ID of the renamer instance
+ ///
+ public int ID { get; }
+
+ ///
+ /// The name of the renamer instance, mostly for user distinction
+ ///
+ public string Name { get; }
+
+ ///
+ /// The type of the renamer, always should be checked against the Renamer ID to ensure that the script should be executable against your renamer.
+ ///
+ public Type? Type { get; }
+}
+
+///
+/// Interface for a renamer configuration type with settings.
+///
+/// The type of the settings.
+public interface IRenamerConfig : IRenamerConfig where T : class
+{
+ ///
+ /// The settings for the renamer
+ ///
+ T Settings { get; set; }
+}
diff --git a/Shoko.Plugin.Abstractions/ISettingsProvider.cs b/Shoko.Plugin.Abstractions/ISettingsProvider.cs
index e749708ce..33bc36e19 100644
--- a/Shoko.Plugin.Abstractions/ISettingsProvider.cs
+++ b/Shoko.Plugin.Abstractions/ISettingsProvider.cs
@@ -1,7 +1,14 @@
-namespace Shoko.Plugin.Abstractions
+
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// Plugin settings provider.
+///
+public interface ISettingsProvider
{
- public interface ISettingsProvider
- {
- void SaveSettings(IPluginSettings settings);
- }
-}
\ No newline at end of file
+ ///
+ /// Save the plugin settings.
+ ///
+ /// Settings to save.
+ void SaveSettings(IPluginSettings settings);
+}
diff --git a/Shoko.Plugin.Abstractions/IShokoEventHandler.cs b/Shoko.Plugin.Abstractions/IShokoEventHandler.cs
index 9cb63a7c8..d3b3bcab7 100644
--- a/Shoko.Plugin.Abstractions/IShokoEventHandler.cs
+++ b/Shoko.Plugin.Abstractions/IShokoEventHandler.cs
@@ -1,61 +1,80 @@
using System;
+using Shoko.Plugin.Abstractions.Events;
-namespace Shoko.Plugin.Abstractions
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// Interface for Shoko event handlers.
+///
+public interface IShokoEventHandler
{
- public interface IShokoEventHandler
- {
- ///
- /// Fired when a file is deleted and removed from Shoko.
- ///
- event EventHandler FileDeleted;
- ///
- /// Fired when a file is detected, either during a forced import/scan or a watched folder.
- /// Nothing has been done with the file yet here.
- ///
- event EventHandler FileDetected;
- ///
- /// Fired when a file is hashed. Has hashes and stuff.
- ///
- event EventHandler FileHashed;
- ///
- /// Fired when a file is scanned but no changes to the cross-refernce
- /// were made. It can be because the file is unrecognized, or because
- /// there was no changes to the existing cross-references linked to the
- /// file.
- ///
- event EventHandler FileNotMatched;
- ///
- /// Fired when a cross reference is made and data is gathered for a file. This has most if not all relevant data for a file. TvDB may take longer.
- /// Use with a filter on the data source to ensure the desired data is gathered.
- ///
- event EventHandler FileMatched;
- ///
- /// Fired when a file is renamed
- ///
- event EventHandler FileRenamed;
- ///
- /// Fired when a file is moved
- ///
- event EventHandler FileMoved;
- ///
- /// Fired when an AniDB Ban happens...and it will.
- ///
- event EventHandler AniDBBanned;
- ///
- /// Fired on series info updates. Currently, AniDB, TvDB, etc will trigger this.
- ///
- event EventHandler SeriesUpdated;
- ///
- /// Fired on episode info updates. Currently, AniDB, TvDB, etc will trigger this.
- ///
- event EventHandler EpisodeUpdated;
- ///
- /// Fired when the core settings has been saved.
- ///
- event EventHandler SettingsSaved;
- ///
- /// Fired when an avdump event occurs.
- ///
- event EventHandler AVDumpEvent;
- }
+ ///
+ /// Fired when a file is deleted and removed from Shoko.
+ ///
+ event EventHandler FileDeleted;
+
+ ///
+ /// Fired when a file is detected, either during a forced import/scan or a watched folder.
+ /// Nothing has been done with the file yet here.
+ ///
+ event EventHandler FileDetected;
+
+ ///
+ /// Fired when a file is hashed. Has hashes and stuff.
+ ///
+ event EventHandler FileHashed;
+
+ ///
+ /// Fired when a file is scanned but no changes to the cross-reference
+ /// were made. It can be because the file is unrecognized, or because
+ /// there was no changes to the existing cross-references linked to the
+ /// file.
+ ///
+ event EventHandler FileNotMatched;
+
+ ///
+ /// Fired when a cross reference is made and data is gathered for a file. This has most if not all relevant data for a file.
+ /// Use with a filter on the data source to ensure the desired data is gathered.
+ ///
+ event EventHandler FileMatched;
+
+ ///
+ /// Fired when a file is renamed
+ ///
+ event EventHandler FileRenamed;
+
+ ///
+ /// Fired when a file is moved
+ ///
+ event EventHandler FileMoved;
+
+ ///
+ /// Fired when an AniDB Ban happens...and it will.
+ ///
+ event EventHandler AniDBBanned;
+
+ ///
+ /// Fired on series info updates. Currently, AniDB, TMDB, etc will trigger this.
+ ///
+ event EventHandler SeriesUpdated;
+
+ ///
+ /// Fired on episode info updates. Currently, AniDB, TMDB, etc will trigger this.
+ ///
+ event EventHandler EpisodeUpdated;
+
+ ///
+ /// Fired on movie info updates. Currently only TMDB will trigger this.
+ ///
+ event EventHandler MovieUpdated;
+
+ ///
+ /// Fired when the core settings has been saved.
+ ///
+ event EventHandler SettingsSaved;
+
+ ///
+ /// Fired when an avdump event occurs.
+ ///
+ event EventHandler AVDumpEvent;
}
diff --git a/Shoko.Plugin.Abstractions/PluginAttribute.cs b/Shoko.Plugin.Abstractions/PluginAttribute.cs
index 07519ef00..03fe583eb 100644
--- a/Shoko.Plugin.Abstractions/PluginAttribute.cs
+++ b/Shoko.Plugin.Abstractions/PluginAttribute.cs
@@ -1,15 +1,24 @@
using System;
-namespace Shoko.Plugin.Abstractions
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// An attribute for defining a plugin.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class PluginAttribute : Attribute
{
- [AttributeUsage(AttributeTargets.Class)]
- public class PluginAttribute : Attribute
- {
- public string PluginId { get; set; }
+ ///
+ /// The ID of the plugin.
+ ///
+ public string PluginId { get; set; }
- public PluginAttribute(string pluginId)
- {
- PluginId = pluginId;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID of the plugin.
+ public PluginAttribute(string pluginId)
+ {
+ PluginId = pluginId;
}
-}
\ No newline at end of file
+}
diff --git a/Shoko.Plugin.Abstractions/PluginUtilities.cs b/Shoko.Plugin.Abstractions/PluginUtilities.cs
index f027b0928..235d0ce74 100644
--- a/Shoko.Plugin.Abstractions/PluginUtilities.cs
+++ b/Shoko.Plugin.Abstractions/PluginUtilities.cs
@@ -1,47 +1,65 @@
using System;
-namespace Shoko.Plugin.Abstractions
+namespace Shoko.Plugin.Abstractions;
+
+///
+/// Plugin utilities.
+///
+public static class PluginUtilities
{
- public static class PluginUtilities
+ ///
+ /// Remove invalid path characters.
+ ///
+ /// The path.
+ /// The sanitized path.
+ public static string RemoveInvalidPathCharacters(this string path)
{
- public static string RemoveInvalidPathCharacters(this string path)
- {
- string ret = path.Replace(@"*", string.Empty);
- ret = ret.Replace(@"|", string.Empty);
- ret = ret.Replace(@"\", string.Empty);
- ret = ret.Replace(@"/", string.Empty);
- ret = ret.Replace(@":", string.Empty);
- ret = ret.Replace("\"", string.Empty); // double quote
- ret = ret.Replace(@">", string.Empty);
- ret = ret.Replace(@"<", string.Empty);
- ret = ret.Replace(@"?", string.Empty);
- while (ret.EndsWith("."))
- ret = ret.Substring(0, ret.Length - 1);
- return ret.Trim();
- }
+ var ret = path.Replace(@"*", string.Empty);
+ ret = ret.Replace(@"|", string.Empty);
+ ret = ret.Replace(@"\", string.Empty);
+ ret = ret.Replace(@"/", string.Empty);
+ ret = ret.Replace(@":", string.Empty);
+ ret = ret.Replace("\"", string.Empty); // double quote
+ ret = ret.Replace(@">", string.Empty);
+ ret = ret.Replace(@"<", string.Empty);
+ ret = ret.Replace(@"?", string.Empty);
+ while (ret.EndsWith("."))
+ ret = ret.Substring(0, ret.Length - 1);
+ return ret.Trim();
+ }
- public static string ReplaceInvalidPathCharacters(this string path)
- {
- string ret = path.Replace(@"*", "\u2605"); // ★ (BLACK STAR)
- ret = ret.Replace(@"|", "\u00a6"); // ¦ (BROKEN BAR)
- ret = ret.Replace(@"\", "\u29F9"); // ⧹ (BIG REVERSE SOLIDUS)
- ret = ret.Replace(@"/", "\u2044"); // ⁄ (FRACTION SLASH)
- ret = ret.Replace(@":", "\u0589"); // ։ (ARMENIAN FULL STOP)
- ret = ret.Replace("\"", "\u2033"); // ″ (DOUBLE PRIME)
- ret = ret.Replace(@">", "\u203a"); // › (SINGLE RIGHT-POINTING ANGLE QUOTATION MARK)
- ret = ret.Replace(@"<", "\u2039"); // ‹ (SINGLE LEFT-POINTING ANGLE QUOTATION MARK)
- ret = ret.Replace(@"?", "\uff1f"); // ? (FULL WIDTH QUESTION MARK)
- ret = ret.Replace(@"...", "\u2026"); // … (HORIZONTAL ELLIPSIS)
- if (ret.StartsWith(".", StringComparison.Ordinal)) ret = "․" + ret.Substring(1, ret.Length - 1);
- if (ret.EndsWith(".", StringComparison.Ordinal)) // U+002E
- ret = ret.Substring(0, ret.Length - 1) + "․"; // U+2024
- return ret.Trim();
- }
+ ///
+ /// Replace invalid path characters.
+ ///
+ /// The path.
+ /// The sanitized path.
+ public static string ReplaceInvalidPathCharacters(this string path)
+ {
+ var ret = path.Replace(@"*", "\u2605"); // ★ (BLACK STAR)
+ ret = ret.Replace(@"|", "\u00a6"); // ¦ (BROKEN BAR)
+ ret = ret.Replace(@"\", "\u29F9"); // ⧹ (BIG REVERSE SOLIDUS)
+ ret = ret.Replace(@"/", "\u2044"); // ⁄ (FRACTION SLASH)
+ ret = ret.Replace(@":", "\u0589"); // ։ (ARMENIAN FULL STOP)
+ ret = ret.Replace("\"", "\u2033"); // ″ (DOUBLE PRIME)
+ ret = ret.Replace(@">", "\u203a"); // › (SINGLE RIGHT-POINTING ANGLE QUOTATION MARK)
+ ret = ret.Replace(@"<", "\u2039"); // ‹ (SINGLE LEFT-POINTING ANGLE QUOTATION MARK)
+ ret = ret.Replace(@"?", "\uff1f"); // ? (FULL WIDTH QUESTION MARK)
+ ret = ret.Replace(@"...", "\u2026"); // … (HORIZONTAL ELLIPSIS)
+ if (ret.StartsWith(".", StringComparison.Ordinal)) ret = "․" + ret.Substring(1, ret.Length - 1);
+ if (ret.EndsWith(".", StringComparison.Ordinal)) // U+002E
+ ret = ret.Substring(0, ret.Length - 1) + "․"; // U+2024
+ return ret.Trim();
+ }
- public static string PadZeroes(this int num, int total)
- {
- int zeroPadding = total.ToString().Length;
- return num.ToString().PadLeft(zeroPadding, '0');
- }
+ ///
+ /// Pads the number with zeroes and returns it as a string.
+ ///
+ /// Number to pad.
+ /// The highest number that num can be, used to determine how many zeroes to add.
+ /// The padded number as a string.
+ public static string PadZeroes(this int num, int total)
+ {
+ var zeroPadding = total.ToString().Length;
+ return num.ToString().PadLeft(zeroPadding, '0');
}
-}
\ No newline at end of file
+}
diff --git a/Shoko.Plugin.Abstractions/Services/IAniDBService.cs b/Shoko.Plugin.Abstractions/Services/IAniDBService.cs
index 5c5fb22e7..49b232b59 100644
--- a/Shoko.Plugin.Abstractions/Services/IAniDBService.cs
+++ b/Shoko.Plugin.Abstractions/Services/IAniDBService.cs
@@ -1,15 +1,20 @@
namespace Shoko.Plugin.Abstractions.Services;
+///
+/// AniDB service.
+///
public interface IAniDBService
{
///
/// Is the AniDB UDP API currently reachable?
///
public bool IsAniDBUdpReachable { get; }
+
///
/// Are we currently banned from using the AniDB HTTP API?
///
public bool IsAniDBHttpBanned { get; }
+
///
/// Are we currently banned from using the AniDB UDP API?
///
diff --git a/Shoko.Plugin.Abstractions/Services/IConnectivityService.cs b/Shoko.Plugin.Abstractions/Services/IConnectivityService.cs
index a086bf2a9..2a85aba4a 100644
--- a/Shoko.Plugin.Abstractions/Services/IConnectivityService.cs
+++ b/Shoko.Plugin.Abstractions/Services/IConnectivityService.cs
@@ -2,33 +2,33 @@
using System;
using System.Threading.Tasks;
using Shoko.Plugin.Abstractions.Enums;
+using Shoko.Plugin.Abstractions.Events;
-namespace Shoko.Plugin.Abstractions.Services
+namespace Shoko.Plugin.Abstractions.Services;
+
+///
+/// A service used to check or monitor the current network availability.
+///
+public interface IConnectivityService
{
///
- /// A service used to check or monitor the current network availability.
+ /// Dispatched when the network availibility has changed.
///
- public interface IConnectivityService
- {
- ///
- /// Dispatched when the network availibility has changed.
- ///
- event EventHandler NetworkAvailabilityChanged;
+ event EventHandler NetworkAvailabilityChanged;
- ///
- /// Current network availibility.
- ///
- public NetworkAvailability NetworkAvailability { get; }
+ ///
+ /// Current network availibility.
+ ///
+ public NetworkAvailability NetworkAvailability { get; }
- ///
- /// When the last network change was detected.
- ///
- public DateTime LastChangedAt { get; }
+ ///
+ /// When the last network change was detected.
+ ///
+ public DateTime LastChangedAt { get; }
- ///
- /// Check for network availability now.
- ///
- /// The updated network availability status.
- public Task CheckAvailability();
- }
+ ///
+ /// Check for network availability now.
+ ///
+ /// The updated network availability status.
+ public Task CheckAvailability();
}
diff --git a/Shoko.Plugin.Abstractions/Services/IRelocationService.cs b/Shoko.Plugin.Abstractions/Services/IRelocationService.cs
new file mode 100644
index 000000000..c3466dc82
--- /dev/null
+++ b/Shoko.Plugin.Abstractions/Services/IRelocationService.cs
@@ -0,0 +1,25 @@
+using Shoko.Plugin.Abstractions.DataModels;
+using Shoko.Plugin.Abstractions.Events;
+
+namespace Shoko.Plugin.Abstractions.Services;
+
+///
+/// Relocation service.
+///
+public interface IRelocationService
+{
+ ///
+ /// Get the first destination with enough space for the given file, if any
+ ///
+ /// The relocation event args
+ ///
+ IImportFolder? GetFirstDestinationWithSpace(RelocationEventArgs args);
+
+ ///
+ /// Check if the given import folder has enough space for the given file.
+ ///
+ /// The import folder
+ /// The file
+ ///
+ bool ImportFolderHasSpace(IImportFolder folder, IVideoFile file);
+}
diff --git a/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj b/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj
index 1ad31bacb..af8c0013a 100644
--- a/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj
+++ b/Shoko.Plugin.Abstractions/Shoko.Plugin.Abstractions.csproj
@@ -4,6 +4,7 @@
Library
latest
true
+ true
Interfaces for Shoko Plugins
Shoko Team
https://shokoanime.com
@@ -11,8 +12,8 @@
icon.png
https://github.com/ShokoAnime/ShokoServer
plugins, shoko, anime, metadata, tagging
- File Events
- 3.0.0-alpha9
+ Renamer Rewrite
+ 4.0.0
Debug;Release;Benchmarks
AnyCPU;x64
false
diff --git a/Shoko.Server.sln b/Shoko.Server.sln
index d6a57bb23..de0fbdaa8 100644
--- a/Shoko.Server.sln
+++ b/Shoko.Server.sln
@@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.git-blame-ignore-revs = .git-blame-ignore-revs
.gitignore = .gitignore
+ Shoko.Server.sln.DotSettings = Shoko.Server.sln.DotSettings
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shoko.CLI", "Shoko.CLI\Shoko.CLI.csproj", "{3A8E0177-9701-4A59-A6CD-16C6908839EA}"
@@ -41,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\issue-no-response.yml = .github\workflows\issue-no-response.yml
.github\workflows\ReplaceAVD3URL.ps1 = .github\workflows\ReplaceAVD3URL.ps1
.github\workflows\ReplaceSentryDSN.ps1 = .github\workflows\ReplaceSentryDSN.ps1
+ .github\workflows\ReplaceTmdbApiKey.ps1 = .github\workflows\ReplaceTmdbApiKey.ps1
.github\workflows\UploadRelease.ps1 = .github\workflows\UploadRelease.ps1
EndProjectSection
EndProject
diff --git a/Shoko.Server.sln.DotSettings b/Shoko.Server.sln.DotSettings
index fc15ad7c1..8764318c3 100644
--- a/Shoko.Server.sln.DotSettings
+++ b/Shoko.Server.sln.DotSettings
@@ -7,6 +7,10 @@
MD
SHA
DB
+ TMDB
+ DBID
+ TvDB
+ TVDB
HTTP
ID
OS
@@ -23,4 +27,5 @@
True
True
- True
\ No newline at end of file
+ True
+
diff --git a/Shoko.Server/API/APIExtensions.cs b/Shoko.Server/API/APIExtensions.cs
index aae86bad7..786b4690a 100644
--- a/Shoko.Server/API/APIExtensions.cs
+++ b/Shoko.Server/API/APIExtensions.cs
@@ -14,6 +14,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Sentry;
+using Shoko.Plugin.Abstractions.Enums;
using Shoko.Server.API.ActionFilters;
using Shoko.Server.API.Authentication;
using Shoko.Server.API.SignalR;
@@ -24,6 +25,7 @@
using Shoko.Server.API.v3.Models.Shoko;
using Shoko.Server.API.WebUI;
using Shoko.Server.Plugin;
+using Shoko.Server.Services;
using Shoko.Server.Utilities;
using File = System.IO.File;
using AniDBEmitter = Shoko.Server.API.SignalR.Aggregate.AniDBEmitter;
@@ -48,8 +50,8 @@ public static IServiceCollection AddAPI(this IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddScoped();
services.AddScoped();
- services.AddScoped();
services.AddScoped();
services.AddAuthentication(options =>
@@ -121,6 +123,8 @@ public static IServiceCollection AddAPI(this IServiceCollection services)
options.SchemaFilter>();
options.SchemaFilter>();
+ options.SchemaFilter>();
+ options.SchemaFilter>();
options.SchemaFilter>();
options.CustomSchemaIds(GetTypeName);
@@ -204,7 +208,7 @@ private static string GetGenericTypeName(Type genericType)
{
return genericType.Name.Replace("+", ".").Replace("`1", "") + "[" + string.Join(",", genericType.GetGenericArguments().Select(GetTypeName)) + "]";
}
-
+
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo
@@ -269,8 +273,13 @@ public static IApplicationBuilder UseAPI(this IApplicationBuilder app)
DefaultContentType = "text/html",
OnPrepareResponse = ctx =>
{
- ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
- ctx.Context.Response.Headers.Append("Expires", "0");
+ var requestPath = ctx.File.PhysicalPath;
+ // We set the cache headers only for index.html file because it doesn't have a different hash when changed
+ if (requestPath?.EndsWith("index.html", StringComparison.OrdinalIgnoreCase) ?? false)
+ {
+ ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
+ ctx.Context.Response.Headers.Append("Expires", "0");
+ }
}
});
@@ -329,7 +338,7 @@ public static IApplicationBuilder UseAPI(this IApplicationBuilder app)
return app;
}
-
+
private static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target)
{
foreach (var dir in source.GetDirectories())
diff --git a/Shoko.Server/API/APIHelper.cs b/Shoko.Server/API/APIHelper.cs
index f62928774..8d0bfec63 100644
--- a/Shoko.Server/API/APIHelper.cs
+++ b/Shoko.Server/API/APIHelper.cs
@@ -1,7 +1,8 @@
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
-using Shoko.Models.Enums;
+using Shoko.Plugin.Abstractions.Enums;
using Shoko.Server.API.Authentication;
using Shoko.Server.API.v3.Models.Common;
using Shoko.Server.Models;
@@ -12,13 +13,8 @@ namespace Shoko.Server.API;
public static class APIHelper
{
- public static string ConstructImageLinkFromTypeAndId(HttpContext ctx, int type, int id, bool short_url = true)
- {
- var imgType = (ImageEntityType)type;
- return ProperURL(ctx,
- $"/api/v3/image/{Image.GetSourceFromType(imgType)}/{Image.GetSimpleTypeFromImageType(imgType)}/{id}",
- short_url);
- }
+ public static string ConstructImageLinkFromTypeAndId(HttpContext ctx, ImageEntityType imageType, DataSourceEnum dataType, int id, bool short_url = true)
+ => ProperURL(ctx, $"/api/v3/Image/{dataType.ToV3Dto()}/{imageType.ToV3Dto()}/{id}", short_url);
public static string ProperURL(HttpContext ctx, string path, bool short_url = false)
{
@@ -32,17 +28,26 @@ public static string ProperURL(HttpContext ctx, string path, bool short_url = fa
return string.Empty;
}
+ // Only get the user once from the db for the same request, and let the GC
+ // automagically clean up the user object reference mapping when the request
+ // is disposed.
+ private static readonly ConditionalWeakTable _userTable = [];
+
public static SVR_JMMUser GetUser(this ClaimsPrincipal identity)
{
if (!ServerState.Instance.ServerOnline) return InitUser.Instance;
var authenticated = identity?.Identity?.IsAuthenticated ?? false;
if (!authenticated) return null;
+ if (_userTable.TryGetValue(identity, out var user))
+ return user;
var nameIdentifier = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
if (nameIdentifier == null) return null;
if (!int.TryParse(nameIdentifier, out var id) || id == 0) return null;
- return RepoFactory.JMMUser.GetByID(id);
+ user = RepoFactory.JMMUser.GetByID(id);
+ _userTable.AddOrUpdate(identity, user);
+ return user;
}
public static SVR_JMMUser GetUser(this HttpContext ctx)
diff --git a/Shoko.Server/API/Resolvers/EmitEmptyEnumerableInsteadOfNullResolver.cs b/Shoko.Server/API/Resolvers/EmitEmptyEnumerableInsteadOfNullResolver.cs
index bf3cf2b7f..87ade0b4d 100644
--- a/Shoko.Server/API/Resolvers/EmitEmptyEnumerableInsteadOfNullResolver.cs
+++ b/Shoko.Server/API/Resolvers/EmitEmptyEnumerableInsteadOfNullResolver.cs
@@ -31,9 +31,12 @@ public override void OnActionExecuted(ActionExecutedContext ctx)
}
// It would be nice if we could cache this somehow, but IDK
- objectResult.Formatters.Add(new NewtonsoftJsonOutputFormatter(SerializerSettings,
+ objectResult.Formatters.Add(new NewtonsoftJsonOutputFormatter(
+ SerializerSettings,
ctx.HttpContext.RequestServices.GetRequiredService>(),
- MvcOptions));
+ MvcOptions,
+ null
+ ));
}
}
diff --git a/Shoko.Server/API/SignalR/Aggregate/AVDumpEmitter.cs b/Shoko.Server/API/SignalR/Aggregate/AVDumpEmitter.cs
index 4c3ebad0a..6214ca87d 100644
--- a/Shoko.Server/API/SignalR/Aggregate/AVDumpEmitter.cs
+++ b/Shoko.Server/API/SignalR/Aggregate/AVDumpEmitter.cs
@@ -3,6 +3,7 @@
using System.Linq;
using Microsoft.AspNetCore.SignalR;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.API.SignalR.Models;
using Shoko.Server.Utilities;
diff --git a/Shoko.Server/API/SignalR/Aggregate/AniDBEmitter.cs b/Shoko.Server/API/SignalR/Aggregate/AniDBEmitter.cs
index 20faf598c..b47c86aeb 100644
--- a/Shoko.Server/API/SignalR/Aggregate/AniDBEmitter.cs
+++ b/Shoko.Server/API/SignalR/Aggregate/AniDBEmitter.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.SignalR;
+using Shoko.Server.API.SignalR.Models;
using Shoko.Server.Providers.AniDB;
using Shoko.Server.Providers.AniDB.Interfaces;
@@ -27,32 +28,20 @@ public void Dispose()
private async void OnUDPStateUpdate(object sender, AniDBStateUpdate e)
{
- await SendAsync("AniDBUDPStateUpdate", e);
+ await SendAsync("AniDBUDPStateUpdate", new AniDBStatusUpdateSignalRModel(e));
}
private async void OnHttpStateUpdate(object sender, AniDBStateUpdate e)
{
- await SendAsync("AniDBHttpStateUpdate", e);
+ await SendAsync("AniDBHttpStateUpdate", new AniDBStatusUpdateSignalRModel(e));
}
public override object GetInitialMessage()
{
- return new List
+ return new List
{
- new()
- {
- UpdateType = UpdateType.UDPBan,
- UpdateTime = UDPHandler.BanTime ?? DateTime.Now,
- Value = UDPHandler.IsBanned,
- PauseTimeSecs = (int)TimeSpan.FromHours(UDPHandler.BanTimerResetLength).TotalSeconds
- },
- new()
- {
- UpdateType = UpdateType.HTTPBan,
- UpdateTime = HttpHandler.BanTime ?? DateTime.Now,
- Value = HttpHandler.IsBanned,
- PauseTimeSecs = (int)TimeSpan.FromHours(HttpHandler.BanTimerResetLength).TotalSeconds
- }
+ new(UpdateType.UDPBan, UDPHandler.IsBanned, UDPHandler.BanTime ?? DateTime.Now, (int)TimeSpan.FromHours(UDPHandler.BanTimerResetLength).TotalSeconds),
+ new(UpdateType.HTTPBan, HttpHandler.IsBanned, HttpHandler.BanTime ?? DateTime.Now, (int)TimeSpan.FromHours(HttpHandler.BanTimerResetLength).TotalSeconds),
};
}
}
diff --git a/Shoko.Server/API/SignalR/Aggregate/NetworkEmitter.cs b/Shoko.Server/API/SignalR/Aggregate/NetworkEmitter.cs
index 595e09ffa..cb569aa79 100644
--- a/Shoko.Server/API/SignalR/Aggregate/NetworkEmitter.cs
+++ b/Shoko.Server/API/SignalR/Aggregate/NetworkEmitter.cs
@@ -1,6 +1,7 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Plugin.Abstractions.Services;
using Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Aggregate/ShokoEventEmitter.cs b/Shoko.Server/API/SignalR/Aggregate/ShokoEventEmitter.cs
index fe0b2c2dd..1809a35ca 100644
--- a/Shoko.Server/API/SignalR/Aggregate/ShokoEventEmitter.cs
+++ b/Shoko.Server/API/SignalR/Aggregate/ShokoEventEmitter.cs
@@ -1,6 +1,7 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.API.SignalR.Models;
namespace Shoko.Server.API.SignalR.Aggregate;
@@ -21,6 +22,7 @@ public ShokoEventEmitter(IHubContext hub, IShokoEventHandler event
EventHandler.FileDeleted += OnFileDeleted;
EventHandler.SeriesUpdated += OnSeriesUpdated;
EventHandler.EpisodeUpdated += OnEpisodeUpdated;
+ EventHandler.MovieUpdated += OnMovieUpdated;
}
public void Dispose()
@@ -34,6 +36,7 @@ public void Dispose()
EventHandler.FileDeleted -= OnFileDeleted;
EventHandler.SeriesUpdated -= OnSeriesUpdated;
EventHandler.EpisodeUpdated -= OnEpisodeUpdated;
+ EventHandler.MovieUpdated -= OnMovieUpdated;
}
private async void OnFileDetected(object sender, FileDetectedEventArgs e)
@@ -41,19 +44,19 @@ private async void OnFileDetected(object sender, FileDetectedEventArgs e)
await SendAsync("FileDetected", new FileDetectedEventSignalRModel(e));
}
- private async void OnFileDeleted(object sender, FileDeletedEventArgs e)
+ private async void OnFileDeleted(object sender, FileEventArgs e)
{
- await SendAsync("FileDeleted", new FileDeletedEventSignalRModel(e));
+ await SendAsync("FileDeleted", new FileEventSignalRModel(e));
}
- private async void OnFileHashed(object sender, FileHashedEventArgs e)
+ private async void OnFileHashed(object sender, FileEventArgs e)
{
- await SendAsync("FileHashed", new FileHashedEventSignalRModel(e));
+ await SendAsync("FileHashed", new FileEventSignalRModel(e));
}
- private async void OnFileMatched(object sender, FileMatchedEventArgs e)
+ private async void OnFileMatched(object sender, FileEventArgs e)
{
- await SendAsync("FileMatched", new FileMatchedEventSignalRModel(e));
+ await SendAsync("FileMatched", new FileEventSignalRModel(e));
}
private async void OnFileRenamed(object sender, FileRenamedEventArgs e)
@@ -81,6 +84,11 @@ private async void OnEpisodeUpdated(object sender, EpisodeInfoUpdatedEventArgs e
await SendAsync("EpisodeUpdated", new EpisodeInfoUpdatedEventSignalRModel(e));
}
+ private async void OnMovieUpdated(object sender, MovieInfoUpdatedEventArgs e)
+ {
+ await SendAsync("MovieUpdated", new MovieInfoUpdatedEventSignalRModel(e));
+ }
+
public override object GetInitialMessage()
{
// No back data for this
diff --git a/Shoko.Server/API/SignalR/Legacy/AniDBEmitter.cs b/Shoko.Server/API/SignalR/Legacy/AniDBEmitter.cs
index b59f0243c..f7f59105d 100644
--- a/Shoko.Server/API/SignalR/Legacy/AniDBEmitter.cs
+++ b/Shoko.Server/API/SignalR/Legacy/AniDBEmitter.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
+using Shoko.Server.API.SignalR.Models;
using Shoko.Server.Providers.AniDB;
using Shoko.Server.Providers.AniDB.Interfaces;
@@ -33,21 +34,21 @@ public async Task OnConnectedAsync(IClientProxy caller)
await caller.SendAsync("AniDBState", new Dictionary
{
{"UDPBanned", UDPHandler.IsBanned},
- {"UDPBanTime", UDPHandler.BanTime},
+ {"UDPBanTime", UDPHandler.BanTime?.ToUniversalTime()},
{"UDPBanWaitPeriod", UDPHandler.BanTimerResetLength},
{"HttpBanned", HttpHandler.IsBanned},
- {"HttpBanTime", HttpHandler.BanTime},
+ {"HttpBanTime", HttpHandler.BanTime?.ToUniversalTime()},
{"HttpBanWaitPeriod", HttpHandler.BanTimerResetLength},
});
}
private async void OnUDPStateUpdate(object sender, AniDBStateUpdate e)
{
- await Hub.Clients.All.SendAsync("AniDBUDPStateUpdate", e);
+ await Hub.Clients.All.SendAsync("AniDBUDPStateUpdate", new AniDBStatusUpdateSignalRModel(e));
}
private async void OnHttpStateUpdate(object sender, AniDBStateUpdate e)
{
- await Hub.Clients.All.SendAsync("AniDBHttpStateUpdate", e);
+ await Hub.Clients.All.SendAsync("AniDBHttpStateUpdate", new AniDBStatusUpdateSignalRModel(e));
}
}
diff --git a/Shoko.Server/API/SignalR/Legacy/ShokoEventEmitter.cs b/Shoko.Server/API/SignalR/Legacy/ShokoEventEmitter.cs
index 4229ceebc..6045690ec 100644
--- a/Shoko.Server/API/SignalR/Legacy/ShokoEventEmitter.cs
+++ b/Shoko.Server/API/SignalR/Legacy/ShokoEventEmitter.cs
@@ -1,6 +1,7 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.API.SignalR.Models;
namespace Shoko.Server.API.SignalR.Legacy;
@@ -19,9 +20,11 @@ public ShokoEventEmitter(IHubContext hub, IShokoEventHandler even
EventHandler.FileMatched += OnFileMatched;
EventHandler.FileRenamed += OnFileRenamed;
EventHandler.FileMoved += OnFileMoved;
+ EventHandler.FileDeleted += OnFileDeleted;
EventHandler.FileNotMatched += OnFileNotMatched;
EventHandler.SeriesUpdated += OnSeriesUpdated;
EventHandler.EpisodeUpdated += OnEpisodeUpdated;
+ EventHandler.MovieUpdated += OnMovieUpdated;
}
public void Dispose()
@@ -31,9 +34,11 @@ public void Dispose()
EventHandler.FileMatched -= OnFileMatched;
EventHandler.FileRenamed -= OnFileRenamed;
EventHandler.FileMoved -= OnFileMoved;
+ EventHandler.FileDeleted -= OnFileDeleted;
EventHandler.FileNotMatched -= OnFileNotMatched;
EventHandler.SeriesUpdated -= OnSeriesUpdated;
EventHandler.EpisodeUpdated -= OnEpisodeUpdated;
+ EventHandler.MovieUpdated -= OnMovieUpdated;
}
private async void OnFileDetected(object sender, FileDetectedEventArgs e)
@@ -41,14 +46,19 @@ private async void OnFileDetected(object sender, FileDetectedEventArgs e)
await Hub.Clients.All.SendAsync("FileDetected", new FileDetectedEventSignalRModel(e));
}
- private async void OnFileHashed(object sender, FileHashedEventArgs e)
+ private async void OnFileDeleted(object sender, FileEventArgs e)
{
- await Hub.Clients.All.SendAsync("FileHashed", new FileHashedEventSignalRModel(e));
+ await Hub.Clients.All.SendAsync("FileDeleted", new FileEventSignalRModel(e));
}
- private async void OnFileMatched(object sender, FileMatchedEventArgs e)
+ private async void OnFileHashed(object sender, FileEventArgs e)
{
- await Hub.Clients.All.SendAsync("FileMatched", new FileMatchedEventSignalRModel(e));
+ await Hub.Clients.All.SendAsync("FileHashed", new FileEventSignalRModel(e));
+ }
+
+ private async void OnFileMatched(object sender, FileEventArgs e)
+ {
+ await Hub.Clients.All.SendAsync("FileMatched", new FileEventSignalRModel(e));
}
private async void OnFileRenamed(object sender, FileRenamedEventArgs e)
@@ -75,4 +85,9 @@ private async void OnEpisodeUpdated(object sender, EpisodeInfoUpdatedEventArgs e
{
await Hub.Clients.All.SendAsync("EpisodeUpdated", new EpisodeInfoUpdatedEventSignalRModel(e));
}
+
+ private async void OnMovieUpdated(object sender, MovieInfoUpdatedEventArgs e)
+ {
+ await Hub.Clients.All.SendAsync("MovieUpdated", new MovieInfoUpdatedEventSignalRModel(e));
+ }
}
diff --git a/Shoko.Server/API/SignalR/Models/AVDumpEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/AVDumpEventSignalRModel.cs
index 3712e34aa..30eca2c69 100644
--- a/Shoko.Server/API/SignalR/Models/AVDumpEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/AVDumpEventSignalRModel.cs
@@ -4,6 +4,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.Utilities;
#nullable enable
diff --git a/Shoko.Server/API/SignalR/Models/AniDBStatusUpdateSignalRModel.cs b/Shoko.Server/API/SignalR/Models/AniDBStatusUpdateSignalRModel.cs
new file mode 100644
index 000000000..a13d24b02
--- /dev/null
+++ b/Shoko.Server/API/SignalR/Models/AniDBStatusUpdateSignalRModel.cs
@@ -0,0 +1,51 @@
+using System;
+using Shoko.Server.Providers.AniDB;
+
+#nullable enable
+namespace Shoko.Server.API.SignalR.Models;
+
+public class AniDBStatusUpdateSignalRModel
+{
+ ///
+ /// The value of the UpdateType, is the ban active, is it waiting on a response, etc
+ ///
+ public bool Value { get; set; }
+
+ ///
+ /// Auxiliary Message for some states
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Update type, Ban, Invalid Session, Waiting on Response, etc
+ ///
+ public UpdateType UpdateType { get; set; }
+
+ ///
+ /// When was it updated, usually Now, but may not be
+ ///
+ public DateTime UpdateTime { get; set; }
+
+ ///
+ /// If we are pausing the queue, then for how long(er)
+ ///
+ public int PauseTimeSecs { get; set; }
+
+ public AniDBStatusUpdateSignalRModel(UpdateType updateType, bool value, DateTime updateTime, int pauseTimeSecs, string message = "")
+ {
+ Value = value;
+ Message = message;
+ UpdateType = updateType;
+ UpdateTime = updateTime.ToUniversalTime();
+ PauseTimeSecs = pauseTimeSecs;
+ }
+
+ public AniDBStatusUpdateSignalRModel(AniDBStateUpdate update)
+ {
+ Value = update.Value;
+ Message = update.Message;
+ UpdateType = update.UpdateType;
+ UpdateTime = update.UpdateTime.ToUniversalTime();
+ PauseTimeSecs = update.PauseTimeSecs;
+ }
+}
diff --git a/Shoko.Server/API/SignalR/Models/EpisodeInfoUpdatedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/EpisodeInfoUpdatedEventSignalRModel.cs
index 8c010be26..037eb1cc9 100644
--- a/Shoko.Server/API/SignalR/Models/EpisodeInfoUpdatedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/EpisodeInfoUpdatedEventSignalRModel.cs
@@ -1,10 +1,9 @@
using System.Collections.Generic;
-using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Shoko.Plugin.Abstractions;
using Shoko.Plugin.Abstractions.Enums;
-using Shoko.Server.Models;
+using Shoko.Plugin.Abstractions.Events;
#nullable enable
namespace Shoko.Server.API.SignalR.Models;
@@ -17,29 +16,8 @@ public EpisodeInfoUpdatedEventSignalRModel(EpisodeInfoUpdatedEventArgs eventArgs
Reason = eventArgs.Reason;
EpisodeID = eventArgs.EpisodeInfo.ID;
SeriesID = eventArgs.SeriesInfo.ID;
- ShokoEpisodeIDs = [];
- ShokoSeriesIDs = [];
- ShokoGroupIDs = [];
- // TODO: Add support for more metadata sources when they're hooked up internally.
- switch (Source)
- {
- case DataSourceEnum.Shoko when eventArgs.EpisodeInfo is SVR_AnimeEpisode shokoEpisode:
- {
- ShokoEpisodeIDs = [shokoEpisode.AnimeEpisodeID];
- ShokoSeriesIDs = [shokoEpisode.AnimeSeriesID];
- if (eventArgs.SeriesInfo is SVR_AnimeSeries series)
- ShokoGroupIDs = series.AllGroupsAbove.Select(g => g.AnimeGroupID).ToArray();
- }
- break;
- case DataSourceEnum.AniDB when eventArgs.EpisodeInfo is SVR_AniDB_Episode anidbEpisode && anidbEpisode.AnimeEpisode is SVR_AnimeEpisode shokoEpisode:
- {
- ShokoEpisodeIDs = [shokoEpisode.AnimeEpisodeID];
- ShokoSeriesIDs = [shokoEpisode.AnimeSeriesID];
- if (shokoEpisode.AnimeSeries is SVR_AnimeSeries series)
- ShokoGroupIDs = series.AllGroupsAbove.Select(g => g.AnimeGroupID).ToArray();
- }
- break;
- }
+ ShokoEpisodeIDs = eventArgs.EpisodeInfo.ShokoEpisodeIDs;
+ ShokoSeriesIDs = eventArgs.SeriesInfo.ShokoSeriesIDs;
}
///
@@ -73,9 +51,4 @@ public EpisodeInfoUpdatedEventSignalRModel(EpisodeInfoUpdatedEventArgs eventArgs
/// Shoko series ids affected by this update.
///
public IReadOnlyList ShokoSeriesIDs { get; }
-
- ///
- /// Shoko group ids affected by this update.
- ///
- public IReadOnlyList ShokoGroupIDs { get; }
}
diff --git a/Shoko.Server/API/SignalR/Models/FileDeletedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileDeletedEventSignalRModel.cs
deleted file mode 100644
index 46dd6b568..000000000
--- a/Shoko.Server/API/SignalR/Models/FileDeletedEventSignalRModel.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Shoko.Plugin.Abstractions;
-
-#nullable enable
-namespace Shoko.Server.API.SignalR.Models;
-
-public class FileDeletedEventSignalRModel : FileEventSignalRModel
-{
- public FileDeletedEventSignalRModel(FileDeletedEventArgs eventArgs) : base(eventArgs) { }
-}
diff --git a/Shoko.Server/API/SignalR/Models/FileDetectedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileDetectedEventSignalRModel.cs
index 6b8427193..ed1271887 100644
--- a/Shoko.Server/API/SignalR/Models/FileDetectedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/FileDetectedEventSignalRModel.cs
@@ -1,4 +1,5 @@
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
#nullable enable
namespace Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Models/FileEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileEventSignalRModel.cs
index a07ae3479..84320e315 100644
--- a/Shoko.Server/API/SignalR/Models/FileEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/FileEventSignalRModel.cs
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
+using Shoko.Commons.Extensions;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.Models;
#nullable enable
@@ -11,28 +13,26 @@ public class FileEventSignalRModel
public FileEventSignalRModel(FileEventArgs eventArgs)
{
RelativePath = eventArgs.RelativePath;
- FileID = eventArgs.FileInfo.VideoID;
- FileLocationID = eventArgs.FileInfo.ID;
+ FileID = eventArgs.File.VideoID;
+ FileLocationID = eventArgs.File.ID;
ImportFolderID = eventArgs.ImportFolder.ID;
- var xrefs = eventArgs.VideoInfo.CrossReferences;
- var episodeDict = eventArgs.EpisodeInfo
- .Cast()
- .Select(e => e.AnimeEpisode)
- .Where(e => e != null)
- .ToDictionary(e => e!.AniDB_EpisodeID, e => e!);
+ var xrefs = eventArgs.Video.CrossReferences;
+ var episodeDict = eventArgs.Episodes
+ .WhereNotNull()
+ .ToDictionary(e => e.AnidbEpisodeID, e => e);
var animeToGroupDict = episodeDict.Values
- .DistinctBy(e => e.AnimeSeriesID)
- .Select(e => e.AnimeSeries)
- .Where(s => s != null)
- .ToDictionary(s => s.AniDB_ID, s => (s.AnimeSeriesID, s.AnimeGroupID));
+ .DistinctBy(e => e.SeriesID)
+ .Select(e => e.Series)
+ .WhereNotNull()
+ .ToDictionary(s => s.AnidbAnimeID, s => (s.ID, s.ParentGroupID));
CrossReferences = xrefs
.Select(xref => new FileCrossReferenceSignalRModel
{
- EpisodeID = episodeDict.TryGetValue(xref.AnidbEpisodeID, out var shokoEpisode) ? shokoEpisode.AnimeEpisodeID : null,
+ EpisodeID = episodeDict.TryGetValue(xref.AnidbEpisodeID, out var shokoEpisode) ? shokoEpisode.ID : null,
AnidbEpisodeID = xref.AnidbEpisodeID,
- SeriesID = animeToGroupDict.TryGetValue(xref.AnidbAnimeID, out var tuple) ? tuple.AnimeSeriesID : null,
+ SeriesID = animeToGroupDict.TryGetValue(xref.AnidbAnimeID, out var tuple) ? tuple.ID : null,
AnidbAnimeID = xref.AnidbAnimeID,
- GroupID = animeToGroupDict.TryGetValue(xref.AnidbAnimeID, out tuple) ? tuple.AnimeGroupID : null,
+ GroupID = animeToGroupDict.TryGetValue(xref.AnidbAnimeID, out tuple) ? tuple.ParentGroupID : null,
})
.ToList();
}
diff --git a/Shoko.Server/API/SignalR/Models/FileHashedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileHashedEventSignalRModel.cs
deleted file mode 100644
index e29865b9f..000000000
--- a/Shoko.Server/API/SignalR/Models/FileHashedEventSignalRModel.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Shoko.Plugin.Abstractions;
-
-#nullable enable
-namespace Shoko.Server.API.SignalR.Models;
-
-public class FileHashedEventSignalRModel : FileEventSignalRModel
-{
- public FileHashedEventSignalRModel(FileHashedEventArgs eventArgs) : base(eventArgs) { }
-}
diff --git a/Shoko.Server/API/SignalR/Models/FileMatchedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileMatchedEventSignalRModel.cs
deleted file mode 100644
index 3ecca0a67..000000000
--- a/Shoko.Server/API/SignalR/Models/FileMatchedEventSignalRModel.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Shoko.Plugin.Abstractions;
-using Shoko.Server.Models;
-
-#nullable enable
-namespace Shoko.Server.API.SignalR.Models;
-
-public class FileMatchedEventSignalRModel : FileEventSignalRModel
-{
- public FileMatchedEventSignalRModel(FileMatchedEventArgs eventArgs) : base(eventArgs) { }
-}
diff --git a/Shoko.Server/API/SignalR/Models/FileMovedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileMovedEventSignalRModel.cs
index 8435a9b09..88c75ea88 100644
--- a/Shoko.Server/API/SignalR/Models/FileMovedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/FileMovedEventSignalRModel.cs
@@ -1,4 +1,5 @@
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
namespace Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Models/FileNotMatchedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileNotMatchedEventSignalRModel.cs
index e7a3fd105..6a6b99998 100644
--- a/Shoko.Server/API/SignalR/Models/FileNotMatchedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/FileNotMatchedEventSignalRModel.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
using Shoko.Server.Models;
namespace Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Models/FileRenamedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/FileRenamedEventSignalRModel.cs
index cd14f4363..1f7189e9c 100644
--- a/Shoko.Server/API/SignalR/Models/FileRenamedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/FileRenamedEventSignalRModel.cs
@@ -1,4 +1,5 @@
using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Events;
namespace Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Models/MovieInfoUpdatedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/MovieInfoUpdatedEventSignalRModel.cs
new file mode 100644
index 000000000..8fbe5f627
--- /dev/null
+++ b/Shoko.Server/API/SignalR/Models/MovieInfoUpdatedEventSignalRModel.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Shoko.Plugin.Abstractions;
+using Shoko.Plugin.Abstractions.Enums;
+using Shoko.Plugin.Abstractions.Events;
+
+#nullable enable
+namespace Shoko.Server.API.SignalR.Models;
+
+public class MovieInfoUpdatedEventSignalRModel
+{
+ public MovieInfoUpdatedEventSignalRModel(MovieInfoUpdatedEventArgs eventArgs)
+ {
+ Source = eventArgs.MovieInfo.Source;
+ Reason = eventArgs.Reason;
+ MovieID = eventArgs.MovieInfo.ID;
+ ShokoEpisodeIDs = eventArgs.MovieInfo.ShokoEpisodeIDs;
+ ShokoSeriesIDs = eventArgs.MovieInfo.ShokoSeriesIDs;
+ }
+
+ ///
+ /// The provider metadata source.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public DataSourceEnum Source { get; }
+
+ ///
+ /// The update reason.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public UpdateReason Reason { get; }
+
+ ///
+ /// The provided metadata movie id.
+ ///
+ public int MovieID { get; }
+
+ ///
+ /// Shoko episode ids affected by this update.
+ ///
+ public IReadOnlyList ShokoEpisodeIDs { get; }
+
+ ///
+ /// Shoko series ids affected by this update.
+ ///
+ public IReadOnlyList ShokoSeriesIDs { get; }
+}
diff --git a/Shoko.Server/API/SignalR/Models/NetworkAvailabilitySignalRModel.cs b/Shoko.Server/API/SignalR/Models/NetworkAvailabilitySignalRModel.cs
index f15187693..0c53bf3a7 100644
--- a/Shoko.Server/API/SignalR/Models/NetworkAvailabilitySignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/NetworkAvailabilitySignalRModel.cs
@@ -3,6 +3,7 @@
using Newtonsoft.Json.Converters;
using Shoko.Plugin.Abstractions;
using Shoko.Plugin.Abstractions.Enums;
+using Shoko.Plugin.Abstractions.Events;
namespace Shoko.Server.API.SignalR.Models;
diff --git a/Shoko.Server/API/SignalR/Models/QueueStateSignalRModel.cs b/Shoko.Server/API/SignalR/Models/QueueStateSignalRModel.cs
index 1c21532f2..62245673c 100644
--- a/Shoko.Server/API/SignalR/Models/QueueStateSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/QueueStateSignalRModel.cs
@@ -35,5 +35,5 @@ public class QueueStateSignalRModel
///
/// The currently executing jobs and their details
///
- public List CurrentlyExecuting { get; set; }
+ public List CurrentlyExecuting { get; set; } = [];
}
diff --git a/Shoko.Server/API/SignalR/Models/SeriesInfoUpdatedEventSignalRModel.cs b/Shoko.Server/API/SignalR/Models/SeriesInfoUpdatedEventSignalRModel.cs
index caf795c5f..676ba54b6 100644
--- a/Shoko.Server/API/SignalR/Models/SeriesInfoUpdatedEventSignalRModel.cs
+++ b/Shoko.Server/API/SignalR/Models/SeriesInfoUpdatedEventSignalRModel.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
+using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Shoko.Plugin.Abstractions;
using Shoko.Plugin.Abstractions.Enums;
+using Shoko.Plugin.Abstractions.Events;
#nullable enable
namespace Shoko.Server.API.SignalR.Models;
@@ -14,9 +16,8 @@ public SeriesInfoUpdatedEventSignalRModel(SeriesInfoUpdatedEventArgs eventArgs)
Source = eventArgs.SeriesInfo.Source;
Reason = eventArgs.Reason;
SeriesID = eventArgs.SeriesInfo.ID;
-
ShokoSeriesIDs = eventArgs.SeriesInfo.ShokoSeriesIDs;
- ShokoGroupIDs = eventArgs.SeriesInfo.ShokoGroupIDs;
+ Episodes = eventArgs.Episodes.Select(e => new SeriesInfoUpdatedEventEpisodeDetailsSignalRModel(e)).ToList();
}
///
@@ -42,7 +43,33 @@ public SeriesInfoUpdatedEventSignalRModel(SeriesInfoUpdatedEventArgs eventArgs)
public IReadOnlyList ShokoSeriesIDs { get; }
///
- /// Shoko group ids affected by this update.
+ /// The episodes that were added/updated/removed during this event.
///
- public IReadOnlyList ShokoGroupIDs { get; }
+ public IReadOnlyList Episodes { get; }
+
+ public class SeriesInfoUpdatedEventEpisodeDetailsSignalRModel
+ {
+ ///
+ /// The update reason.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public UpdateReason Reason { get; }
+
+ ///
+ /// The provided metadata episode id.
+ ///
+ public int EpisodeID { get; }
+
+ ///
+ /// Shoko episode ids affected by this update.
+ ///
+ public IReadOnlyList ShokoEpisodeIDs { get; }
+
+ public SeriesInfoUpdatedEventEpisodeDetailsSignalRModel(EpisodeInfoUpdatedEventArgs eventArgs)
+ {
+ Reason = eventArgs.Reason;
+ EpisodeID = eventArgs.EpisodeInfo.ID;
+ ShokoEpisodeIDs = eventArgs.EpisodeInfo.ShokoEpisodeIDs;
+ }
+ }
}
diff --git a/Shoko.Server/API/SignalR/NLog/SignalRTarget.cs b/Shoko.Server/API/SignalR/NLog/SignalRTarget.cs
index 174d5ccbc..efc54e14b 100644
--- a/Shoko.Server/API/SignalR/NLog/SignalRTarget.cs
+++ b/Shoko.Server/API/SignalR/NLog/SignalRTarget.cs
@@ -39,7 +39,6 @@ public SignalRTarget()
ConnectMethodName = "GetBacklog";
MaxLogsCount = 10;
Logs = new List(MaxLogsCount);
- OptimizeBufferReuse = true;
}
protected override void Write(LogEventInfo logEvent)
diff --git a/Shoko.Server/API/WebUI/WebUIHelper.cs b/Shoko.Server/API/WebUI/WebUIHelper.cs
index f45e96358..2fdf1ce19 100644
--- a/Shoko.Server/API/WebUI/WebUIHelper.cs
+++ b/Shoko.Server/API/WebUI/WebUIHelper.cs
@@ -1,35 +1,56 @@
using System;
using System.IO;
using System.Net;
+using System.Net.Http;
+using System.Text.RegularExpressions;
using Newtonsoft.Json;
using SharpCompress.Common;
using SharpCompress.Readers;
using Shoko.Server.Utilities;
+#nullable enable
namespace Shoko.Server.API.WebUI;
-public static class WebUIHelper
+public static partial class WebUIHelper
{
+ [GeneratedRegex(@"^[a-z0-9_\-\.]+/[a-z0-9_\-\.]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex CompiledRepoNameRegex();
+
+ private static string? _clientRepoName = null;
+
+ public static string ClientRepoName =>
+ _clientRepoName ??= Environment.GetEnvironmentVariable("SHOKO_CLIENT_REPO") is { } envVar && CompiledRepoNameRegex().IsMatch(envVar) ? envVar : "ShokoAnime/Shoko-WebUI";
+
+ private static string? _serverRepoName = null;
+
+ public static string ServerRepoName =>
+ _serverRepoName ??= Environment.GetEnvironmentVariable("SHOKO_SERVER_REPO") is { } envVar && CompiledRepoNameRegex().IsMatch(envVar) ? envVar : "ShokoAnime/ShokoServer";
+
///
/// Web UI Version Info.
///
- public record WebUIVersionInfo {
+ public record WebUIVersionInfo
+ {
///
/// Package version.
///
- public string package = "1.0.0";
+ [JsonProperty("package")]
+ public string Package { get; set; } = "1.0.0";
///
/// Short-form git commit sha digest.
///
- public string git = "0000000";
+ [JsonProperty("git")]
+ public string Git { get; set; } = "0000000";
///
/// True if this is a debug package.
///
- public bool debug = false;
+ [JsonProperty("debug")]
+ public bool Debug { get; set; } = false;
///
/// Release date for web ui release.
///
- public DateTime? date = null;
+ [JsonProperty("date")]
+ public DateTime? Date { get; set; } = null;
}
///
@@ -43,14 +64,15 @@ public static void GetUrlAndUpdate(string tagName)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
var release = DownloadApiResponse($"releases/tags/{tagName}");
- string url = null;
+ if (release is null)
+ return;
+
+ string? url = null;
foreach (var assets in release.assets)
{
// We don't care what the zip is named, only that it is attached.
- // This is because we changed the signature from "latest.zip" to
- // "Shoko-WebUI-{obj.tag_name}.zip" in the upgrade to web ui v2
string fileName = assets.name;
- if (fileName == "latest.zip" || fileName == $"Shoko-WebUI-{release.tag_name}.zip")
+ if (Path.GetExtension(fileName) is ".zip")
{
url = assets.browser_download_url;
break;
@@ -76,7 +98,6 @@ private static void DownloadAndInstallUpdate(string url, DateTime releaseDate)
{
var webuiDir = Path.Combine(Utils.ApplicationPath, "webui");
var backupDir = Path.Combine(webuiDir, "old");
- var zipFile = Path.Combine(webuiDir, "update.zip");
var files = Directory.GetFiles(webuiDir);
var directories = Directory.GetDirectories(webuiDir);
@@ -85,11 +106,9 @@ private static void DownloadAndInstallUpdate(string url, DateTime releaseDate)
Directory.CreateDirectory(webuiDir);
// Download the zip file.
- using (var client = new WebClient())
- {
- client.Headers.Add("User-Agent", $"ShokoServer/{Utils.GetApplicationVersion()}");
- client.DownloadFile(url, zipFile);
- }
+ using var client = new HttpClient();
+ client.DefaultRequestHeaders.Add("User-Agent", $"ShokoServer/{Utils.GetApplicationVersion()}");
+ var zipContent = client.GetByteArrayAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
// Remove any old lingering backups.
if (Directory.Exists(backupDir))
@@ -110,32 +129,27 @@ private static void DownloadAndInstallUpdate(string url, DateTime releaseDate)
// Also move all the files directly in the base directory into the backup directory until the update is complete.
foreach (var file in files)
{
- if (file == zipFile || !File.Exists(file))
- continue;
var newFile = file.Replace(webuiDir, backupDir);
File.Move(file, newFile);
}
// Extract the zip contents into the folder.
- using (var stream = new FileStream(zipFile, FileMode.Open))
- using (var reader = ReaderFactory.Open(stream))
+ using var stream = new MemoryStream(zipContent);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
{
- while (reader.MoveToNextEntry())
+ if (!reader.Entry.IsDirectory)
{
- if (!reader.Entry.IsDirectory)
+ reader.WriteEntryToDirectory(webuiDir, new ExtractionOptions
{
- reader.WriteEntryToDirectory(webuiDir, new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = true
- });
- }
+ ExtractFullPath = true,
+ Overwrite = true
+ });
}
}
// Clean up the now unneeded backup and zip file because we have an updated install.
Directory.Delete(backupDir, true);
- File.Delete(zipFile);
// Add release date to json
AddReleaseDate(releaseDate);
@@ -147,21 +161,21 @@ private static void AddReleaseDate(DateTime releaseDate)
if (webUIFileInfo.Exists)
{
// Load the web ui version info from disk.
- var webuiVersion = Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(webUIFileInfo.FullName));
+ var webuiVersion = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(webUIFileInfo.FullName));
// Set the release data and save the info again if the date is not set.
- if (!webuiVersion.date.HasValue)
+ if (webuiVersion is not null && !webuiVersion.Date.HasValue)
{
- webuiVersion.date = releaseDate;
- System.IO.File.WriteAllText(webUIFileInfo.FullName, Newtonsoft.Json.JsonConvert.SerializeObject(webuiVersion));
+ webuiVersion.Date = releaseDate;
+ File.WriteAllText(webUIFileInfo.FullName, JsonConvert.SerializeObject(webuiVersion));
}
}
}
- public static WebUIVersionInfo LoadWebUIVersionInfo()
+ public static WebUIVersionInfo? LoadWebUIVersionInfo()
{
var webUIFileInfo = new FileInfo(Path.Combine(Utils.ApplicationPath, "webui/version.json"));
if (webUIFileInfo.Exists)
- return Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(webUIFileInfo.FullName));
+ return JsonConvert.DeserializeObject(File.ReadAllText(webUIFileInfo.FullName));
return null;
}
@@ -171,14 +185,14 @@ public static WebUIVersionInfo LoadWebUIVersionInfo()
/// do version have to be stable
/// An error occurred while downloading the resource.
///
- public static string WebUIGetLatestVersion(bool stable)
+ public static string? WebUIGetLatestVersion(bool stable)
{
// The 'latest' release will always be a stable release, so we can skip
// checking it if we're looking for a pre-release.
if (!stable)
return GetVersionTag(false);
var release = DownloadApiResponse("releases/latest");
- return release.tag_name;
+ return release?.tag_name;
}
///
@@ -188,9 +202,12 @@ public static string WebUIGetLatestVersion(bool stable)
/// do version have to be stable
/// An error occurred while downloading the resource.
///
- private static string GetVersionTag(bool stable)
+ private static string? GetVersionTag(bool stable)
{
var releases = DownloadApiResponse("releases");
+ if (releases is null)
+ return null;
+
foreach (var release in releases)
{
// Filter out pre-releases from the stable release channel, but don't
@@ -219,14 +236,14 @@ private static string GetVersionTag(bool stable)
/// Repository name.
///
/// An error occurred while downloading the resource.
- internal static dynamic DownloadApiResponse(string endpoint, string repoName = null)
+ internal static dynamic DownloadApiResponse(string endpoint, string? repoName = null)
{
- if (string.IsNullOrEmpty(repoName))
- repoName = "shokoanime/shoko-webui";
- var client = new WebClient();
- client.Headers.Add("Accept: application/vnd.github.v3+json");
- client.Headers.Add("User-Agent", $"ShokoServer/{Utils.GetApplicationVersion()}");
- var response = client.DownloadString(new Uri($"https://api.github.com/repos/{repoName}/{endpoint}"));
- return JsonConvert.DeserializeObject(response);
+ repoName ??= ClientRepoName;
+ using var client = new HttpClient();
+ client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
+ client.DefaultRequestHeaders.Add("User-Agent", $"ShokoServer/{Utils.GetApplicationVersion()}");
+ var response = client.GetStringAsync(new Uri($"https://api.github.com/repos/{repoName}/{endpoint}"))
+ .ConfigureAwait(false).GetAwaiter().GetResult();
+ return JsonConvert.DeserializeObject(response)!;
}
}
diff --git a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
index 12b38ca91..9e4623a09 100644
--- a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
+++ b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs
@@ -6,34 +6,41 @@
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
+using Shoko.Commons.Extensions;
+using Shoko.Server.Extensions;
using Shoko.Server.Utilities;
#nullable enable
namespace Shoko.Server.API.WebUI;
-public static class WebUIThemeProvider
+public static partial class WebUIThemeProvider
{
- private static Regex VersionRegex = new Regex(@"^\s*(?\d+)(?:\.(?\d+)(?:\.(?\d+)(?:\.(?\d+))?)?)?\s*$", RegexOptions.ECMAScript | RegexOptions.Compiled);
+ [GeneratedRegex(@"^\s*(?\d+)(?:\.(?\d+)(?:\.(?\d+)(?:\.(?\d+))?)?)?\s*$", RegexOptions.ECMAScript | RegexOptions.Compiled)]
+ private static partial Regex VersionRegex();
- private static Regex FileNameRegex = new Regex(@"^\b[A-Za-z][A-Za-z0-9_\-]*\b$", RegexOptions.ECMAScript | RegexOptions.Compiled);
+ [GeneratedRegex(@"^\b[A-Za-z][A-Za-z0-9_\-]*\b$", RegexOptions.Compiled | RegexOptions.ECMAScript)]
+ private static partial Regex FileNameRegex();
- private static ISet AllowedMIMEs = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "application/json", "text/json", "text/plain" };
+ private static readonly ISet _allowedJsonMime = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "application/json", "text/json", "text/plain" };
- private static DateTime? NextRefreshAfter = null;
+ private static readonly ISet _allowedCssMime = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "text/css", "text/plain" };
- private static Dictionary? ThemeDict = null;
+ private static DateTime? _nextRefreshAfter = null;
+
+ private static Dictionary? _themeDict = null;
private static Dictionary RefreshThemes(bool forceRefresh = false)
{
- if (ThemeDict == null || forceRefresh || DateTime.UtcNow > NextRefreshAfter)
+ if (_themeDict == null || forceRefresh || DateTime.UtcNow > _nextRefreshAfter)
{
- NextRefreshAfter = DateTime.UtcNow.AddMinutes(10);
- ThemeDict = ThemeDefinition.FromDirectory("themes").ToDictionary(theme => theme.ID);
+ _nextRefreshAfter = DateTime.UtcNow.AddMinutes(10);
+ _themeDict = ThemeDefinition.FromThemesDirectory().ToDictionary(theme => theme.ID);
}
- return ThemeDict;
+ return _themeDict;
}
///
@@ -47,7 +54,7 @@ public static IEnumerable GetThemes(bool forceRefresh = false)
}
///
- /// Get a spesified theme from the theme folder.
+ /// Get a specified theme from the theme folder.
///
/// The id of the theme to get.
/// Forcefully refresh the theme dict. before checking for the theme.
@@ -64,12 +71,16 @@ public static IEnumerable GetThemes(bool forceRefresh = false)
/// A boolean indicating the success status of the operation.
public static bool RemoveTheme(ThemeDefinition theme)
{
- var filePath = Path.Combine(Utils.ApplicationPath, "themes", theme.FileName);
- if (!File.Exists(filePath))
+ var jsonFilePath = Path.Combine(Utils.ApplicationPath, "themes", theme.JsonFileName);
+ var cssFilePath = Path.Combine(Utils.ApplicationPath, "themes", theme.CssFileName);
+ if (!File.Exists(jsonFilePath) && !File.Exists(cssFilePath))
return false;
- File.Delete(filePath);
- ThemeDict?.Remove(theme.ID);
+ _themeDict?.Remove(theme.ID);
+ if (File.Exists(jsonFilePath))
+ File.Delete(jsonFilePath);
+ if (File.Exists(cssFilePath))
+ File.Delete(cssFilePath);
return true;
}
@@ -80,16 +91,13 @@ public static bool RemoveTheme(ThemeDefinition theme)
/// The theme to update.
/// Flag indicating whether to enable preview mode.
/// The updated theme metadata.
- public static async Task UpdateTheme(ThemeDefinition theme, bool preview = false)
+ public static async Task UpdateThemeOnline(ThemeDefinition theme, bool preview = false)
{
// Return the local theme if we don't have an update url.
- if (string.IsNullOrEmpty(theme.URL))
- if (preview)
- throw new ValidationException("No update URL in existing theme definition.");
- else
- return theme;
+ if (string.IsNullOrEmpty(theme.UpdateUrl))
+ return theme;
- if (!(Uri.TryCreate(theme.URL, UriKind.Absolute, out var updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
+ if (!(Uri.TryCreate(theme.UpdateUrl, UriKind.Absolute, out var updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
throw new ValidationException("Invalid update URL in existing theme definition.");
using var httpClient = new HttpClient();
@@ -102,7 +110,7 @@ public static async Task UpdateTheme(ThemeDefinition theme, boo
// Check if the response is using the correct content-type.
var contentType = response.Content.Headers.ContentType?.MediaType;
- if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
+ if (string.IsNullOrEmpty(contentType) || !_allowedJsonMime.Contains(contentType))
throw new HttpRequestException("Invalid content-type. Expected JSON.");
// Simple sanity check before parsing the response content.
@@ -112,23 +120,53 @@ public static async Task UpdateTheme(ThemeDefinition theme, boo
throw new HttpRequestException("Invalid theme file format.");
// Try to parse the updated theme.
- var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, theme.FileName, preview) ??
+ var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, preview) ??
throw new HttpRequestException("Failed to parse the updated theme.");
- // Save the updated theme file if we're not pre-viewing.
- if (!preview)
+ if (updatedTheme.Version <= theme.Version)
+ throw new ValidationException("New theme version is lower than the existing theme version.");
+
+ if (!(Uri.TryCreate(updatedTheme.UpdateUrl, UriKind.Absolute, out updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
+ throw new ValidationException("Invalid update URL in new theme definition.");
+
+ if (!string.IsNullOrEmpty(updatedTheme.CssUrl))
{
- var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
- if (!Directory.Exists(dirPath))
- Directory.CreateDirectory(dirPath);
+ if (!Uri.TryCreate(updatedTheme.CssUrl, UriKind.RelativeOrAbsolute, out var cssUrl))
+ throw new ValidationException("Invalid CSS URL in new theme definition.");
- var filePath = Path.Combine(dirPath, theme.FileName);
- await File.WriteAllTextAsync(filePath, content);
+ if (!cssUrl.IsAbsoluteUri)
+ {
+ if (updateUrl is null)
+ throw new ValidationException("Unable to resolve CSS URL in new theme definition because it is relative and no update URL was provided.");
+ cssUrl = new Uri(updateUrl, cssUrl);
+ updatedTheme.CssUrl = cssUrl.AbsoluteUri;
+ }
+
+ if (cssUrl.Scheme != Uri.UriSchemeHttp && cssUrl.Scheme != Uri.UriSchemeHttps)
+ throw new ValidationException("Invalid CSS URL in existing theme definition.");
+
+ if (!string.IsNullOrEmpty(theme.CssContent))
+ throw new ValidationException("Theme already has CSS overrides inlined. Remove URL or inline CSS first before proceeding.");
+
+ var cssResponse = await httpClient.GetAsync(theme.CssUrl);
+ if (cssResponse.StatusCode != HttpStatusCode.OK)
+ throw new HttpRequestException($"Failed to retrieve CSS file with status code {cssResponse.StatusCode}.", null, cssResponse.StatusCode);
- if (ThemeDict != null && !ThemeDict.TryAdd(theme.ID, updatedTheme))
- ThemeDict[theme.ID] = updatedTheme;
+ var cssContentType = cssResponse.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(cssContentType) || !_allowedCssMime.Contains(cssContentType))
+ throw new ValidationException("Invalid css content-type for resource. Expected \"text/css\" or \"text/plain\".");
+
+ var cssContent = (await cssResponse.Content.ReadAsStringAsync())?.Trim();
+ if (string.IsNullOrEmpty(cssContent))
+ throw new ValidationException("The css url cannot resolve to an empty resource if it is provided in the theme definition.");
+
+ updatedTheme.CssContent = cssContent;
}
+ // Save the updated theme file if we're not pre-viewing.
+ if (!preview)
+ await SaveTheme(updatedTheme);
+
return updatedTheme;
}
@@ -138,7 +176,7 @@ public static async Task UpdateTheme(ThemeDefinition theme, boo
/// The URL leading to where the theme lives online.
/// Flag indicating whether to enable preview mode.
/// The new or updated theme metadata.
- public static async Task InstallTheme(string url, bool preview = false)
+ public static async Task InstallThemeFromUrl(string url, bool preview = false)
{
if (!(Uri.TryCreate(url, UriKind.Absolute, out var updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
throw new ValidationException("Invalid repository URL.");
@@ -155,11 +193,11 @@ public static async Task InstallTheme(string url, bool preview
// We _want_ it to be JSON.
if (!string.Equals(extName, ".json", StringComparison.InvariantCultureIgnoreCase))
- throw new ValidationException("Invalid theme file format. Expected JSON.");
+ throw new ValidationException("Invalid theme file name extension. Expected '.json'.");
// Check if the file name conforms to our specified format.
var fileName = Path.GetFileNameWithoutExtension(lastFragment);
- if (string.IsNullOrEmpty(fileName) || !FileNameRegex.IsMatch(fileName))
+ if (string.IsNullOrEmpty(fileName) || !FileNameRegex().IsMatch(fileName))
throw new ValidationException("Invalid theme file name.");
using var httpClient = new HttpClient();
@@ -172,79 +210,183 @@ public static async Task InstallTheme(string url, bool preview
// Check if the response is using the correct content-type.
var contentType = response.Content.Headers.ContentType?.MediaType;
- if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType))
- throw new HttpRequestException("Invalid content-type. Expected JSON.");
+ if (string.IsNullOrEmpty(contentType) || !_allowedJsonMime.Contains(contentType))
+ throw new HttpRequestException("Invalid content-type. Expected 'application/json', 'text/json', or 'text/plain.");
// Simple sanity check before parsing the response content.
var content = await response.Content.ReadAsStringAsync();
+ return await InstallOrUpdateThemeFromJson(content, fileName, preview);
+ }
+
+ public static async Task InstallOrUpdateThemeFromJson(string? content, string fileName, bool preview = false)
+ {
+ fileName = Path.GetFileNameWithoutExtension(fileName);
+ if (string.IsNullOrEmpty(fileName) || !FileNameRegex().IsMatch(fileName))
+ throw new ValidationException("Invalid theme file name.");
+
content = content?.Trim();
if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[^1] != '}')
- throw new HttpRequestException("Invalid theme file format.");
+ throw new HttpRequestException("Pre-validation failed. Resource is not a valid JSON object.");
// Try to parse the new theme.
var id = FileNameToID(fileName);
- var theme = ThemeDefinition.FromJson(content, id, fileName + extName, preview) ??
- throw new HttpRequestException("Failed to parse the new theme.");
+ var theme = ThemeDefinition.FromJson(content, id, preview) ??
+ throw new HttpRequestException("Failed to parse the theme from resource.");
- // Save the new theme file if we're not pre-viewing.
- if (!preview)
+ Uri? updateUrl = null;
+ if (!string.IsNullOrEmpty(theme.UpdateUrl) && !(Uri.TryCreate(theme.UpdateUrl, UriKind.Absolute, out updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps)))
+ throw new ValidationException("Invalid update URL in new theme definition.");
+
+ if (!string.IsNullOrEmpty(theme.CssUrl))
{
- var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
- if (!Directory.Exists(dirPath))
- Directory.CreateDirectory(dirPath);
+ if (!Uri.TryCreate(theme.CssUrl, UriKind.RelativeOrAbsolute, out var cssUrl))
+ throw new ValidationException("Invalid CSS URL in new theme definition.");
+
+ if (!cssUrl.IsAbsoluteUri)
+ {
+ if (updateUrl is null)
+ throw new ValidationException("Unable to resolve CSS URL in theme definition because it is relative and no update URL was provided.");
+ cssUrl = new Uri(updateUrl, cssUrl);
+ theme.CssUrl = cssUrl.AbsoluteUri;
+ }
- var filePath = Path.Combine(dirPath, fileName + extName);
- await File.WriteAllTextAsync(filePath, content);
+ if (cssUrl.Scheme != Uri.UriSchemeHttp && cssUrl.Scheme != Uri.UriSchemeHttps)
+ throw new ValidationException("Invalid CSS URL in theme definition.");
- if (ThemeDict != null && !ThemeDict.TryAdd(id, theme))
- ThemeDict[id] = theme;
+ using var httpClient = new HttpClient();
+ httpClient.Timeout = TimeSpan.FromMinutes(1);
+ var cssResponse = await httpClient.GetAsync(theme.CssUrl);
+ if (cssResponse.StatusCode != HttpStatusCode.OK)
+ throw new HttpRequestException($"Failed to retrieve CSS file with status code {cssResponse.StatusCode}.", null, cssResponse.StatusCode);
+
+ var cssContentType = cssResponse.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(cssContentType) || !_allowedCssMime.Contains(cssContentType))
+ throw new ValidationException("Invalid css content-type for resource. Expected \"text/css\" or \"text/plain\".");
+
+ var cssContent = await cssResponse.Content.ReadAsStringAsync();
+ if (string.IsNullOrWhiteSpace(cssContent))
+ throw new ValidationException("The css url cannot resolve to an empty resource if it is provided in the theme definition.");
+
+ theme.CssContent = cssContent;
}
+ if (theme.Values.Count == 0 && string.IsNullOrWhiteSpace(theme.CssContent))
+ throw new ValidationException("The theme definition cannot be empty.");
+
+ // Save the new theme file if we're not pre-viewing.
+ if (!preview)
+ await SaveTheme(theme);
+
+ return theme;
+ }
+
+ public static async Task CreateOrUpdateThemeFromCss(string content, string fileName, bool preview = false)
+ {
+ if (string.IsNullOrEmpty(fileName) || !FileNameRegex().IsMatch(fileName))
+ throw new ValidationException("Invalid theme file name.");
+
+ if (string.IsNullOrWhiteSpace(content))
+ throw new ValidationException("The theme definition cannot be empty.");
+
+ var id = FileNameToID(fileName);
+ var theme = GetTheme(id, true) ?? new(id, preview);
+ if (!string.IsNullOrEmpty(theme.UpdateUrl))
+ throw new ValidationException("Unable to manually update a theme with an update URL set.");
+ theme.CssContent = content;
+
+ // Save the new theme file if we're not pre-viewing.
+ if (!preview)
+ await SaveTheme(theme);
+
return theme;
}
+ private static async Task SaveTheme(ThemeDefinition theme)
+ {
+ var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
+ if (!Directory.Exists(dirPath))
+ Directory.CreateDirectory(dirPath);
+
+ var cssContent = theme.CssContent;
+ var cssFilePath = Path.Combine(dirPath, theme.CssFileName);
+ if (!string.IsNullOrEmpty(cssContent))
+ await File.WriteAllTextAsync(cssFilePath, cssContent);
+ else if (File.Exists(cssFilePath))
+ File.Delete(cssFilePath);
+
+ var jsonContent = theme.ToJson();
+ var jsonFilePath = Path.Combine(dirPath, theme.JsonFileName);
+ if (!string.IsNullOrEmpty(jsonContent))
+ await File.WriteAllTextAsync(jsonFilePath, theme.ToJson());
+ else if (File.Exists(jsonFilePath))
+ File.Delete(jsonFilePath);
+
+ if (_themeDict != null)
+ _themeDict[theme.ID] = theme;
+ }
+
public class ThemeDefinitionInput
{
///
/// The display name of the theme. Will be inferred from the filename if omitted.
///
- public string? Name = null;
+ [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
+ public string? Name { get; set; } = null;
///
/// The theme version.
///
[Required]
- [RegularExpression(@"^\s*(?\d+)(?:\.(?\d+)(?:\.(?\d+)(?:\.(?\d+))?)?)?\s*$")]
- public string Version = string.Empty;
+ [MinLength(1)]
+ [RegularExpression(@"^(?\d+)(?:\.(?\d+)(?:\.(?\d+)(?:\.(?\d+))?)?)?$")]
+ [JsonProperty("version")]
+ public string Version { get; set; } = string.Empty;
///
/// Optional description for the theme, if any.
///
- public string? Description = null;
+ [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
+ public string? Description { get; set; } = null;
- /**
- * Optional tags to make it easier to search for the theme.
- */
- public IReadOnlyList? Tags;
+ ///
+ /// Optional tags to make it easier to search for the theme.
+ ///
+ [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)]
+ public IReadOnlyList? Tags { get; set; } = null;
///
/// The author's name.
///
[Required]
- public string Author = string.Empty;
+ [JsonProperty("author")]
+ public string Author { get; set; } = string.Empty;
///
/// The CSS variables defined in the theme.
///
- [Required]
- [MinLength(1)]
- public IReadOnlyDictionary Values = new Dictionary();
+ [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)]
+ public IReadOnlyDictionary? Values { get; set; } = null;
+
+ ///
+ /// The CSS overrides defined in the theme, if any.
+ ///
+ [JsonProperty("css", NullValueHandling = NullValueHandling.Ignore)]
+ public string? CssContent { get; set; }
+
+ ///
+ /// The URL for where the theme CSS overrides file lives. Will be downloaded locally if provided. It must end in ".css" and the content type must be "text/plain" or "text/css".
+ ///
+ [Url]
+ [JsonProperty("cssUrl", NullValueHandling = NullValueHandling.Ignore)]
+ public string? CssUrl { get; set; }
///
/// The URL for where the theme definition lives. Used for updates.
///
[Url]
- public string? URL;
+ [JsonProperty("updateUrl", NullValueHandling = NullValueHandling.Ignore)]
+ public string? UpdateUrl { get; set; }
+
}
public class ThemeDefinition
@@ -258,9 +400,14 @@ public class ThemeDefinition
public readonly string ID;
///
- /// The file name assosiated with the theme.
+ /// The file name associated with the theme.
+ ///
+ public readonly string JsonFileName;
+
+ ///
+ /// The name of the CSS file associated with the theme.
///
- public readonly string FileName;
+ public string CssFileName => JsonFileName[..^Path.GetExtension(JsonFileName).Length] + ".css";
///
/// The display name of the theme.
@@ -273,14 +420,14 @@ public class ThemeDefinition
public readonly string? Description;
///
- /// Author-defined tags assosiated with the theme.
+ /// Author-defined tags associated with the theme.
///
public readonly IReadOnlyList Tags;
///
/// The name of the author of the theme definition.
///
- public readonly string Author;
+ public readonly string? Author;
///
/// The theme version.
@@ -296,107 +443,117 @@ public class ThemeDefinition
[JsonIgnore]
public readonly IReadOnlyDictionary Values;
+ ///
+ /// The CSS content to define for the theme.
+ ///
+ [JsonIgnore]
+ public string? CssContent { get; internal set; } = string.Empty;
+
+ ///
+ /// The URL for where the theme CSS overrides file lives. Will be downloaded locally if provided. It must end in ".css" and the content type must be "text/plain" or "text/css".
+ ///
+ public string? CssUrl { get; internal set; }
+
///
/// The URL for where the theme definition lives. Used for updates.
///
- public readonly string? URL;
+ public readonly string? UpdateUrl;
///
/// Indicates this is only a preview of the theme metadata and the theme
- /// might not actaully be installed yet.
+ /// might not actually be installed yet.
///
public readonly bool IsPreview;
+ private bool? _isInstalled;
+
///
/// Indicates the theme is installed locally.
///
- public readonly bool IsInstalled;
+ public bool IsInstalled => _isInstalled ??= File.Exists(Path.Combine(Utils.ApplicationPath, "themes", JsonFileName)) || File.Exists(Path.Combine(Utils.ApplicationPath, "themes", CssFileName));
+
+ public ThemeDefinition(string id, bool preview = false)
+ {
+ ID = id;
+ JsonFileName = $"{id}.json";
+ Name = NameFromID(ID);
+ Tags = [];
+ Version = new Version(1, 0, 0, 0);
+ Values = new Dictionary();
+ IsPreview = preview;
+ }
- public ThemeDefinition(ThemeDefinitionInput input, string id, string fileName, bool preview = false)
+ public ThemeDefinition(ThemeDefinitionInput input, string id, bool preview = false)
{
// We use a regex match and parse the result instead of using the built-in version parer
// directly because the built-in parser is more rigged then what we want to support.
- var versionMatch = VersionRegex.Match(input.Version);
+ var versionMatch = VersionRegex().Match(input.Version);
var major = int.Parse(versionMatch.Groups["major"].Value);
var minor = versionMatch.Groups["minor"].Success ? int.Parse(versionMatch.Groups["minor"].Value) : 0;
var build = versionMatch.Groups["build"].Success ? int.Parse(versionMatch.Groups["build"].Value) : 0;
var revision = versionMatch.Groups["revision"].Success ? int.Parse(versionMatch.Groups["build"].Value) : 0;
- if (string.IsNullOrEmpty(fileName))
- fileName = $"{id}.json";
ID = id;
- FileName = fileName;
+ JsonFileName = $"{id}.json";
Name = string.IsNullOrEmpty(input.Name) ? NameFromID(ID) : input.Name;
Description = string.IsNullOrWhiteSpace(input.Description) ? null : input.Description;
- Tags = input.Tags ?? new List();
+ Tags = input.Tags ?? [];
Author = input.Author;
Version = new Version(major, minor, build, revision);
Values = input.Values ?? new Dictionary();
- URL = input.URL;
+ CssUrl = input.CssUrl;
+ CssContent = input.CssContent;
+ UpdateUrl = input.UpdateUrl;
IsPreview = preview;
- IsInstalled = File.Exists(Path.Combine(Utils.ApplicationPath, "themes", fileName));
}
public string ToCSS()
- => $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}";
-
- internal static IReadOnlyList FromDirectory(string dirPath)
{
- // Resolve path relative to the application directory and check if
- // it exists.
- dirPath = Path.Combine(Utils.ApplicationPath, dirPath);
- if (!Directory.Exists(dirPath))
- return new List();
-
- return Directory.GetFiles(dirPath)
- .Select(filePath => FromPath(filePath))
- .OfType()
- .DistinctBy(theme => theme.ID)
- .OrderBy(theme => theme.ID)
- .ToList();
+ var cssFile = Path.Combine(Utils.ApplicationPath, "themes", CssFileName);
+ var css = new StringBuilder()
+ .Append('\n')
+ .Append($".theme-{ID} {{\n");
+ if (Values.Count > 0)
+ css.Append(" " + Values.Select(pair => $" --{pair.Key}: {pair.Value};").Join("\n ") + "\n");
+
+ if (Values.Count > 0 && !string.IsNullOrWhiteSpace(CssContent))
+ css.Append('\n');
+
+ if (!string.IsNullOrWhiteSpace(CssContent))
+ css
+ .Append(" " + CssContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None).Select(line => string.IsNullOrWhiteSpace(line) ? string.Empty : $" {line.TrimEnd()}").Join("\n ") + "\n");
+
+ return css
+ .AppendLine("}\n")
+ .ToString();
}
- internal static ThemeDefinition? FromPath(string filePath)
- {
- // Check file extension.
- var extName = Path.GetExtension(filePath);
- if (string.IsNullOrEmpty(extName) || !string.Equals(extName, ".json", StringComparison.InvariantCultureIgnoreCase))
- return null;
-
- var fileName = Path.GetFileNameWithoutExtension(filePath);
- if (string.IsNullOrEmpty(fileName) || !FileNameRegex.IsMatch(fileName))
- return null;
-
- if (!File.Exists(filePath))
- return null;
-
- // Safely try to read
- string? fileContents;
- try
+ public string? ToJson() => !string.IsNullOrEmpty(Author)
+ ? JsonConvert.SerializeObject(new ThemeDefinitionInput()
{
- fileContents = File.ReadAllText(filePath)?.Trim();
- }
- catch
- {
- return null;
- }
- // Simple sanity check before parsing the file contents.
- if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[^1] != '}')
- return null;
-
- var id = FileNameToID(fileName);
- return FromJson(fileContents, id, Path.GetFileName(filePath));
- }
-
- internal static ThemeDefinition? FromJson(string json, string id, string fileName, bool preview = false)
+ Name = Name,
+ Version = Version.ToString(),
+ Description = Description,
+ Tags = Tags.Count is > 0 ? Tags : null,
+ Author = Author,
+ Values = Values.Count is > 0 ? Values : null,
+ CssUrl = string.IsNullOrEmpty(CssUrl) ? null : CssUrl,
+ UpdateUrl = string.IsNullOrEmpty(UpdateUrl) ? null : UpdateUrl,
+ })
+ : null;
+
+ internal static ThemeDefinition? FromJson(string? json, string id, bool preview = false)
{
try
{
+ // Simple sanity check before parsing the file contents.
+ if (string.IsNullOrWhiteSpace(json) || json[0] != '{' || json[^1] != '}')
+ return null;
var input = JsonConvert.DeserializeObject(json);
if (input == null)
return null;
- var theme = new ThemeDefinition(input, id, fileName, preview);
+ var theme = new ThemeDefinition(input, id, preview);
return theme;
}
catch
@@ -404,6 +561,40 @@ internal static IReadOnlyList FromDirectory(string dirPath)
return null;
}
}
+
+ internal static IReadOnlyList FromThemesDirectory()
+ {
+ var dirPath = Path.Combine(Utils.ApplicationPath, "themes");
+ if (!Directory.Exists(dirPath))
+ return new List();
+
+ var allowedExtensions = new HashSet() { ".json", ".css" };
+ return Directory.GetFiles(dirPath)
+ .GroupBy(a => Path.GetFileNameWithoutExtension(a))
+ .Where(a => !string.IsNullOrEmpty(a.Key) && FileNameRegex().IsMatch(a.Key) && a.Any(b => allowedExtensions.Contains(Path.GetExtension(b))))
+ .Select(FromPath)
+ .WhereNotNull()
+ .DistinctBy(theme => theme.ID)
+ .OrderBy(theme => theme.ID)
+ .ToList();
+ }
+
+ private static ThemeDefinition? FromPath(IGrouping fileDetails)
+ {
+ // Check file extension.
+ var id = FileNameToID(fileDetails.Key);
+ var jsonFile = fileDetails.FirstOrDefault(a => string.Equals(Path.GetExtension(a), ".json", StringComparison.InvariantCultureIgnoreCase));
+ var theme = string.IsNullOrEmpty(jsonFile) ? new(id) : FromJson(File.ReadAllText(jsonFile)?.Trim(), id);
+ if (theme is not null)
+ {
+ var cssFileName = fileDetails.FirstOrDefault(a => string.Equals(Path.GetExtension(a), ".css", StringComparison.InvariantCultureIgnoreCase)) ??
+ Path.Combine(Path.GetDirectoryName(jsonFile)!, theme.CssFileName);
+ if (File.Exists(cssFileName))
+ theme.CssContent = File.ReadAllText(cssFileName);
+ }
+
+ return theme;
+ }
}
private static string FileNameToID(string fileName)
@@ -419,5 +610,5 @@ private static string NameFromID(string id)
);
public static string ToCSS(this IEnumerable list)
- => string.Join(" ", list.Select(theme => theme.ToCSS()));
+ => list.Select(theme => theme.ToCSS()).Join("");
}
diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs
index 518f9165c..84e90504a 100644
--- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs
+++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation.cs
@@ -1,4 +1,4 @@
- using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -18,9 +18,8 @@
using Shoko.Server.Filters.Legacy;
using Shoko.Server.Plex;
using Shoko.Server.Providers.AniDB.Interfaces;
-using Shoko.Server.Providers.MovieDB;
+using Shoko.Server.Providers.TMDB;
using Shoko.Server.Providers.TraktTV;
-using Shoko.Server.Providers.TvDB;
using Shoko.Server.Repositories;
using Shoko.Server.Scheduling;
using Shoko.Server.Scheduling.Jobs.Actions;
@@ -42,29 +41,43 @@ public partial class ShokoServiceImplementation : Controller, IShokoServer
private readonly ILogger _logger;
private readonly AnimeGroupCreator _groupCreator;
private readonly JobFactory _jobFactory;
- private readonly TvDBApiHelper _tvdbHelper;
private readonly TraktTVHelper _traktHelper;
- private readonly MovieDBHelper _movieDBHelper;
+ private readonly TmdbLinkingService _tmdbLinkingService;
+ private readonly TmdbMetadataService _tmdbMetadataService;
+ private readonly TmdbSearchService _tmdbSearchService;
private readonly ISettingsProvider _settingsProvider;
private readonly ISchedulerFactory _schedulerFactory;
private readonly ActionService _actionService;
- private readonly AnimeSeriesService _seriesService;
private readonly AnimeEpisodeService _episodeService;
private readonly VideoLocalService _videoLocalService;
private readonly WatchedStatusService _watchedService;
-
- public ShokoServiceImplementation(TvDBApiHelper tvdbHelper, TraktTVHelper traktHelper, MovieDBHelper movieDBHelper, ISchedulerFactory schedulerFactory, ISettingsProvider settingsProvider, ILogger logger, ActionService actionService, AnimeGroupCreator groupCreator, JobFactory jobFactory, AnimeSeriesService seriesService, AnimeEpisodeService episodeService, WatchedStatusService watchedService, VideoLocalService videoLocalService)
+
+ public ShokoServiceImplementation(
+ TraktTVHelper traktHelper,
+ TmdbLinkingService tmdbLinkingService,
+ TmdbMetadataService tmdbMetadataService,
+ TmdbSearchService tmdbSearchService,
+ ISchedulerFactory schedulerFactory,
+ ISettingsProvider settingsProvider,
+ ILogger logger,
+ ActionService actionService,
+ AnimeGroupCreator groupCreator,
+ JobFactory jobFactory,
+ AnimeEpisodeService episodeService,
+ WatchedStatusService watchedService,
+ VideoLocalService videoLocalService
+ )
{
- _tvdbHelper = tvdbHelper;
_traktHelper = traktHelper;
- _movieDBHelper = movieDBHelper;
+ _tmdbLinkingService = tmdbLinkingService;
+ _tmdbMetadataService = tmdbMetadataService;
+ _tmdbSearchService = tmdbSearchService;
_schedulerFactory = schedulerFactory;
_settingsProvider = settingsProvider;
_logger = logger;
_actionService = actionService;
_groupCreator = groupCreator;
_jobFactory = jobFactory;
- _seriesService = seriesService;
_episodeService = episodeService;
_watchedService = watchedService;
_videoLocalService = videoLocalService;
@@ -131,7 +144,7 @@ public CL_MainChanges GetAllChanges(DateTime date, int userID)
changes[0].LastChange = changes[1].LastChange;
}
- var groupService = Utils.ServiceContainer.GetRequiredService();
+ var groupService = Utils.ServiceContainer.GetRequiredService();
c.Groups.ChangedItems = changes[0]
.ChangedItems.Select(a => RepoFactory.AnimeGroup.GetByID(a))
.Where(a => a != null)
@@ -365,7 +378,7 @@ public CL_Response SaveServerSettings(CL_ServerSettings contractIn)
contract.ErrorMessage += "AniDB Password must have a value" + Environment.NewLine;
}
}
-
+
if (!ushort.TryParse(contractIn.AniDB_AVDumpClientPort, out var newAniDB_AVDumpClientPort))
{
contract.ErrorMessage += "AniDB AVDump port must be a valid port" + Environment.NewLine;
@@ -386,7 +399,6 @@ public CL_Response SaveServerSettings(CL_ServerSettings contractIn)
settings.AniDb.DownloadRelatedAnime = contractIn.AniDB_DownloadRelatedAnime;
settings.AniDb.DownloadReleaseGroups = contractIn.AniDB_DownloadReleaseGroups;
settings.AniDb.DownloadReviews = contractIn.AniDB_DownloadReviews;
- settings.AniDb.DownloadSimilarAnime = contractIn.AniDB_DownloadSimilarAnime;
settings.AniDb.MyList_AddFiles = contractIn.AniDB_MyList_AddFiles;
settings.AniDb.MyList_ReadUnwatched = contractIn.AniDB_MyList_ReadUnwatched;
@@ -403,39 +415,17 @@ public CL_Response SaveServerSettings(CL_ServerSettings contractIn)
(ScheduledUpdateFrequency)contractIn.AniDB_Calendar_UpdateFrequency;
settings.AniDb.Anime_UpdateFrequency =
(ScheduledUpdateFrequency)contractIn.AniDB_Anime_UpdateFrequency;
- settings.AniDb.MyListStats_UpdateFrequency =
- (ScheduledUpdateFrequency)contractIn.AniDB_MyListStats_UpdateFrequency;
settings.AniDb.File_UpdateFrequency =
(ScheduledUpdateFrequency)contractIn.AniDB_File_UpdateFrequency;
settings.AniDb.DownloadCharacters = contractIn.AniDB_DownloadCharacters;
settings.AniDb.DownloadCreators = contractIn.AniDB_DownloadCreators;
- // Web Cache
- settings.WebCache.Address = contractIn.WebCache_Address;
- settings.WebCache.XRefFileEpisode_Get = contractIn.WebCache_XRefFileEpisode_Get;
- settings.WebCache.XRefFileEpisode_Send = contractIn.WebCache_XRefFileEpisode_Send;
- settings.WebCache.TvDB_Get = contractIn.WebCache_TvDB_Get;
- settings.WebCache.TvDB_Send = contractIn.WebCache_TvDB_Send;
- settings.WebCache.Trakt_Get = contractIn.WebCache_Trakt_Get;
- settings.WebCache.Trakt_Send = contractIn.WebCache_Trakt_Send;
-
- // TvDB
- settings.TvDB.AutoLink = contractIn.TvDB_AutoLink;
- settings.TvDB.AutoFanart = contractIn.TvDB_AutoFanart;
- settings.TvDB.AutoFanartAmount = contractIn.TvDB_AutoFanartAmount;
- settings.TvDB.AutoPosters = contractIn.TvDB_AutoPosters;
- settings.TvDB.AutoPostersAmount = contractIn.TvDB_AutoPostersAmount;
- settings.TvDB.AutoWideBanners = contractIn.TvDB_AutoWideBanners;
- settings.TvDB.AutoWideBannersAmount = contractIn.TvDB_AutoWideBannersAmount;
- settings.TvDB.UpdateFrequency = (ScheduledUpdateFrequency)contractIn.TvDB_UpdateFrequency;
- settings.TvDB.Language = contractIn.TvDB_Language;
-
- // MovieDB
- settings.MovieDb.AutoFanart = contractIn.MovieDB_AutoFanart;
- settings.MovieDb.AutoFanartAmount = contractIn.MovieDB_AutoFanartAmount;
- settings.MovieDb.AutoPosters = contractIn.MovieDB_AutoPosters;
- settings.MovieDb.AutoPostersAmount = contractIn.MovieDB_AutoPostersAmount;
+ // TMDB
+ settings.TMDB.AutoDownloadBackdrops = contractIn.MovieDB_AutoFanart;
+ settings.TMDB.MaxAutoBackdrops = contractIn.MovieDB_AutoFanartAmount;
+ settings.TMDB.AutoDownloadPosters = contractIn.MovieDB_AutoPosters;
+ settings.TMDB.MaxAutoPosters = contractIn.MovieDB_AutoPostersAmount;
// Import settings
settings.Import.VideoExtensions = contractIn.VideoExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
@@ -453,19 +443,19 @@ public CL_Response SaveServerSettings(CL_ServerSettings contractIn)
settings.Import.RunOnStart = contractIn.RunImportOnStart;
settings.Import.ScanDropFoldersOnStart = contractIn.ScanDropFoldersOnStart;
- settings.Import.Hash_CRC32 = contractIn.Hash_CRC32;
- settings.Import.Hash_MD5 = contractIn.Hash_MD5;
- settings.Import.Hash_SHA1 = contractIn.Hash_SHA1;
- settings.Import.RenameOnImport = contractIn.Import_RenameOnImport;
- settings.Import.MoveOnImport = contractIn.Import_MoveOnImport;
+ settings.Import.Hasher.CRC = contractIn.Hash_CRC32;
+ settings.Import.Hasher.MD5 = contractIn.Hash_MD5;
+ settings.Import.Hasher.SHA1 = contractIn.Hash_SHA1;
+ settings.Plugins.Renamer.RenameOnImport = contractIn.Import_RenameOnImport;
+ settings.Plugins.Renamer.MoveOnImport = contractIn.Import_MoveOnImport;
settings.Import.SkipDiskSpaceChecks = contractIn.SkipDiskSpaceChecks;
// Language
- settings.LanguagePreference = contractIn.LanguagePreference.Split(',').ToList();
- settings.LanguageUseSynonyms = contractIn.LanguageUseSynonyms;
- settings.EpisodeTitleSource = (DataSourceType)contractIn.EpisodeTitleSource;
- settings.SeriesDescriptionSource = (DataSourceType)contractIn.SeriesDescriptionSource;
- settings.SeriesNameSource = (DataSourceType)contractIn.SeriesNameSource;
+ settings.Language.SeriesTitleLanguageOrder = contractIn.LanguagePreference.Split(',').ToList();
+ settings.Language.UseSynonyms = contractIn.LanguageUseSynonyms;
+ settings.Language.EpisodeTitleSourceOrder = [(DataSourceType)contractIn.EpisodeTitleSource];
+ settings.Language.DescriptionSourceOrder = [(DataSourceType)contractIn.SeriesDescriptionSource];
+ settings.Language.SeriesTitleSourceOrder = [(DataSourceType)contractIn.SeriesNameSource];
// Trakt
settings.TraktTv.Enabled = contractIn.Trakt_IsEnabled;
@@ -755,86 +745,10 @@ public string EnableDisableImage(bool enabled, int imageID, int imageType)
{
try
{
- var imgType = (ImageEntityType)imageType;
- int animeID = 0;
-
- switch (imgType)
- {
- case ImageEntityType.AniDB_Cover:
- var anime = RepoFactory.AniDB_Anime.GetByAnimeID(imageID);
- if (anime == null)
- {
- return "Could not find anime";
- }
-
- anime.ImageEnabled = enabled ? 1 : 0;
- RepoFactory.AniDB_Anime.Save(anime);
- break;
-
- case ImageEntityType.TvDB_Banner:
- var banner = RepoFactory.TvDB_ImageWideBanner.GetByID(imageID);
- if (banner == null)
- {
- return "Could not find image";
- }
-
- banner.Enabled = enabled ? 1 : 0;
- RepoFactory.TvDB_ImageWideBanner.Save(banner);
- animeID = RepoFactory.CrossRef_AniDB_TvDB.GetByTvDBID(banner.SeriesID).FirstOrDefault()?.AniDBID ?? 0;
- break;
-
- case ImageEntityType.TvDB_Cover:
- var poster = RepoFactory.TvDB_ImagePoster.GetByID(imageID);
- if (poster == null)
- {
- return "Could not find image";
- }
-
- poster.Enabled = enabled ? 1 : 0;
- RepoFactory.TvDB_ImagePoster.Save(poster);
- animeID = RepoFactory.CrossRef_AniDB_TvDB.GetByTvDBID(poster.SeriesID).FirstOrDefault()?.AniDBID ?? 0;
- break;
-
- case ImageEntityType.TvDB_FanArt:
- var fanart = RepoFactory.TvDB_ImageFanart.GetByID(imageID);
- if (fanart == null)
- {
- return "Could not find image";
- }
-
- fanart.Enabled = enabled ? 1 : 0;
- RepoFactory.TvDB_ImageFanart.Save(fanart);
- animeID = RepoFactory.CrossRef_AniDB_TvDB.GetByTvDBID(fanart.SeriesID).FirstOrDefault()?.AniDBID ?? 0;
- break;
-
- case ImageEntityType.MovieDB_Poster:
- var moviePoster = RepoFactory.MovieDB_Poster.GetByID(imageID);
- if (moviePoster == null)
- {
- return "Could not find image";
- }
-
- moviePoster.Enabled = enabled ? 1 : 0;
- RepoFactory.MovieDB_Poster.Save(moviePoster);
- animeID = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(moviePoster.MovieId, CrossRefType.MovieDB)?.AnimeID ?? 0;
- break;
+ var it = (CL_ImageEntityType)imageType;
+ if (!ImageUtils.SetEnabled(it.ToServerSource(), it.ToServerType(), imageID, enabled))
+ return "Could not find image";
- case ImageEntityType.MovieDB_FanArt:
- var movieFanart = RepoFactory.MovieDB_Fanart.GetByID(imageID);
- if (movieFanart == null)
- {
- return "Could not find image";
- }
-
- movieFanart.Enabled = enabled ? 1 : 0;
- RepoFactory.MovieDB_Fanart.Save(movieFanart);
- animeID = RepoFactory.CrossRef_AniDB_Other.GetByAnimeIDAndType(movieFanart.MovieId, CrossRefType.MovieDB)?.AnimeID ?? 0;
- break;
- }
-
- var schedulerFactory = Utils.ServiceContainer.GetRequiredService();
- var scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult();
- if (animeID != 0) scheduler.StartJob(a => a.AnimeID = animeID).GetAwaiter().GetResult();
return string.Empty;
}
catch (Exception ex)
@@ -849,59 +763,30 @@ public string SetDefaultImage(bool isDefault, int animeID, int imageID, int imag
{
try
{
- var imgType = (ImageEntityType)imageType;
- var sizeType = ImageSizeType.Poster;
-
- switch (imgType)
- {
- case ImageEntityType.AniDB_Cover:
- case ImageEntityType.TvDB_Cover:
- case ImageEntityType.MovieDB_Poster:
- sizeType = ImageSizeType.Poster;
- break;
-
- case ImageEntityType.TvDB_Banner:
- sizeType = ImageSizeType.WideBanner;
- break;
-
- case ImageEntityType.TvDB_FanArt:
- case ImageEntityType.MovieDB_FanArt:
- sizeType = ImageSizeType.Fanart;
- break;
- }
+ var imageEntityType = ((CL_ImageEntityType)imageType).ToServerType();
+ var dataSource = ((CL_ImageEntityType)imageType).ToServerSource();
+ // Reset the image preference.
if (!isDefault)
{
- // this mean we are removing an image as default
- // which essential means deleting the record
-
- var img =
- RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, sizeType);
- if (img != null)
- {
- RepoFactory.AniDB_Anime_DefaultImage.Delete(img.AniDB_Anime_DefaultImageID);
- }
+ var defaultImage = RepoFactory.AniDB_Anime_PreferredImage.GetByAnidbAnimeIDAndType(animeID, imageEntityType);
+ if (defaultImage != null)
+ RepoFactory.AniDB_Anime_PreferredImage.Delete(defaultImage);
}
+ // Mark the image as the preferred/default for it's type.
else
{
- // making the image the default for it's type (poster, fanart, etc)
- var img =
- RepoFactory.AniDB_Anime_DefaultImage.GetByAnimeIDAndImagezSizeType(animeID, sizeType);
- if (img == null)
- {
- img = new AniDB_Anime_DefaultImage();
- }
-
- img.AnimeID = animeID;
- img.ImageParentID = imageID;
- img.ImageParentType = (int)imgType;
- img.ImageType = (int)sizeType;
- RepoFactory.AniDB_Anime_DefaultImage.Save(img);
+ var defaultImage = RepoFactory.AniDB_Anime_PreferredImage.GetByAnidbAnimeIDAndType(animeID, imageEntityType) ?? new(animeID, imageEntityType);
+ defaultImage.ImageID = imageID;
+ defaultImage.ImageSource = dataSource;
+ RepoFactory.AniDB_Anime_PreferredImage.Save(defaultImage);
}
- var schedulerFactory = Utils.ServiceContainer.GetRequiredService();
- var scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult();
- if (animeID != 0) scheduler.StartJob(a => a.AnimeID = animeID).GetAwaiter().GetResult();
+ if (animeID != 0)
+ {
+ var scheduler = _schedulerFactory.GetScheduler().ConfigureAwait(false).GetAwaiter().GetResult();
+ scheduler.StartJob(a => a.AnimeID = animeID).GetAwaiter().GetResult();
+ }
return string.Empty;
}
@@ -1153,7 +1038,8 @@ public List GetRecommendations(int maxResults, int userID, in
var rec = new CL_Recommendation
{
- BasedOnAnimeID = anime.AnimeID, RecommendedAnimeID = link.SimilarAnimeID
+ BasedOnAnimeID = anime.AnimeID,
+ RecommendedAnimeID = link.SimilarAnimeID,
};
// if we don't have the anime locally. lets assume the anime has a high rating
@@ -1171,9 +1057,9 @@ public List GetRecommendations(int maxResults, int userID, in
// check if we have added this recommendation before
// this might happen where animes are recommended based on different votes
// and could end up with different scores
- if (dictRecs.ContainsKey(rec.RecommendedAnimeID))
+ if (dictRecs.TryGetValue(rec.RecommendedAnimeID, out var recommendation))
{
- if (rec.Score < dictRecs[rec.RecommendedAnimeID].Score)
+ if (rec.Score < recommendation.Score)
{
continue;
}
@@ -1386,17 +1272,17 @@ public List GetCharactersForSeiyuu(int seiyuuID)
try
{
- var seiyuu = RepoFactory.AniDB_Seiyuu.GetByID(seiyuuID);
+ var seiyuu = RepoFactory.AniDB_Creator.GetByID(seiyuuID);
if (seiyuu == null)
{
return chars;
}
- var links = RepoFactory.AniDB_Character_Seiyuu.GetBySeiyuuID(seiyuu.SeiyuuID);
+ var links = RepoFactory.AniDB_Character_Creator.GetByCreatorID(seiyuu.CreatorID);
foreach (var chrSei in links)
{
- var chr = RepoFactory.AniDB_Character.GetByID(chrSei.CharID);
+ var chr = RepoFactory.AniDB_Character.GetByID(chrSei.CharacterID);
if (chr != null)
{
var aniChars =
@@ -1422,11 +1308,11 @@ public List GetCharactersForSeiyuu(int seiyuuID)
}
[HttpGet("AniDB/Seiyuu/{seiyuuID}")]
- public AniDB_Seiyuu GetAniDBSeiyuu(int seiyuuID)
+ public CL_AniDB_Seiyuu GetAniDBSeiyuu(int seiyuuID)
{
try
{
- return RepoFactory.AniDB_Seiyuu.GetByID(seiyuuID);
+ return RepoFactory.AniDB_Creator.GetByCreatorID(seiyuuID)?.ToClient();
}
catch (Exception ex)
{
diff --git a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs
index cd8be591a..1044dcd19 100644
--- a/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs
+++ b/Shoko.Server/API/v1/Implementations/ShokoServiceImplementation/ShokoServiceImplementation_Entities.cs
@@ -30,7 +30,7 @@ public partial class ShokoServiceImplementation : IShokoServer
#region Episodes and Files
///
- /// Finds the previous episode for use int the next unwatched episode
+ /// Finds the previous episode for use int the next unwatched episode.
///
///
///
@@ -41,34 +41,24 @@ public CL_AnimeEpisode_User GetPreviousEpisodeForUnwatched(int animeSeriesID, in
try
{
var nextEp = GetNextUnwatchedEpisode(animeSeriesID, userID);
- if (nextEp == null)
- {
+ if (nextEp is null)
return null;
- }
var epType = nextEp.EpisodeType;
var epNum = nextEp.EpisodeNumber - 1;
if (epNum <= 0)
- {
return null;
- }
var series = RepoFactory.AnimeSeries.GetByID(animeSeriesID);
- if (series == null)
- {
+ if (series is null)
return null;
- }
- var anieps = RepoFactory.AniDB_Episode.GetByAnimeIDAndEpisodeTypeNumber(series.AniDB_ID,
- (EpisodeType)epType,
- epNum);
- if (anieps.Count == 0)
- {
+ var anidbEpisodes = RepoFactory.AniDB_Episode.GetByAnimeIDAndEpisodeTypeNumber(series.AniDB_ID, (EpisodeType)epType, epNum);
+ if (anidbEpisodes.Count == 0)
return null;
- }
- var ep = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(anieps[0].EpisodeID);
+ var ep = RepoFactory.AnimeEpisode.GetByAniDBEpisodeID(anidbEpisodes[0].EpisodeID);
return _episodeService.GetV1Contract(ep, userID);
}
catch (Exception ex)
@@ -85,16 +75,12 @@ public CL_AnimeEpisode_User GetNextUnwatchedEpisode(int animeSeriesID, int userI
{
var seriesService = Utils.ServiceContainer.GetService();
var series = RepoFactory.AnimeSeries.GetByID(animeSeriesID);
- if (series == null)
- {
+ if (series is null)
return null;
- }
- var episode = seriesService.GetNextEpisode(series, userID);
- if (episode == null)
- {
+ var episode = seriesService.GetNextUpEpisode(series, userID, new());
+ if (episode is null)
return null;
- }
return _episodeService.GetV1Contract(episode, userID);
}
@@ -112,15 +98,14 @@ public List GetAllUnwatchedEpisodes(int animeSeriesID, int
try
{
- return
- RepoFactory.AnimeEpisode.GetBySeriesID(animeSeriesID)
- .Where(a => a != null && !a.IsHidden)
- .Select(a => _episodeService.GetV1Contract(a, userID))
- .Where(a => a != null)
- .Where(a => a.WatchedCount == 0)
- .OrderBy(a => a.EpisodeType)
- .ThenBy(a => a.EpisodeNumber)
- .ToList();
+ return RepoFactory.AnimeEpisode.GetBySeriesID(animeSeriesID)
+ .Where(a => a != null && !a.IsHidden)
+ .Select(a => _episodeService.GetV1Contract(a, userID))
+ .Where(a => a != null)
+ .Where(a => a.WatchedCount == 0)
+ .OrderBy(a => a.EpisodeType)
+ .ThenBy(a => a.EpisodeNumber)
+ .ToList();
}
catch (Exception ex)
{
@@ -134,13 +119,13 @@ public CL_AnimeEpisode_User GetNextUnwatchedEpisodeForGroup(int animeGroupID, in
{
try
{
- var grp = RepoFactory.AnimeGroup.GetByID(animeGroupID);
- if (grp == null)
+ var group = RepoFactory.AnimeGroup.GetByID(animeGroupID);
+ if (group is null)
{
return null;
}
- var allSeries = grp.AllSeries.OrderBy(a => a.AirDate).ToList();
+ var allSeries = group.AllSeries.OrderBy(a => a.AirDate).ToList();
foreach (var ser in allSeries)
@@ -168,25 +153,23 @@ public List GetContinueWatchingFilter(int userID, int maxR
try
{
var user = RepoFactory.JMMUser.GetByID(userID);
- if (user == null) return retEps;
+ if (user is null) return retEps;
// find the locked Continue Watching Filter
var lockedGFs = RepoFactory.FilterPreset.GetLockedGroupFilters();
var gf = lockedGFs?.FirstOrDefault(a => a.Name == "Continue Watching");
- if (gf == null) return retEps;
+ if (gf is null) return retEps;
var evaluator = HttpContext.RequestServices.GetRequiredService();
- var groupService = HttpContext.RequestServices.GetRequiredService();
+ var groupService = HttpContext.RequestServices.GetRequiredService();
var comboGroups = evaluator.EvaluateFilter(gf, userID).Select(a => RepoFactory.AnimeGroup.GetByID(a.Key)).Where(a => a != null)
.Select(a => groupService.GetV1Contract(a, userID));
- foreach (var grp in comboGroups)
+ foreach (var group in comboGroups)
{
- var sers = RepoFactory.AnimeSeries.GetByGroupID(grp.AnimeGroupID).OrderBy(a => a.AirDate).ToList();
-
+ var seriesList = RepoFactory.AnimeSeries.GetByGroupID(group.AnimeGroupID).OrderBy(a => a.AirDate).ToList();
var seriesWatching = new List();
-
- foreach (var ser in sers)
+ foreach (var ser in seriesList)
{
if (!user.AllowedSeries(ser)) continue;
@@ -196,7 +179,7 @@ public List