diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index a3847dcdfb..ea2675a3d0 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.8",
+ "version": "9.0.2",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
deleted file mode 100644
index 0b848d9f3c..0000000000
--- a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "Development Jellyfin Server - FFmpeg",
- "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
- // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
- // reads the extensions list and installs them
- "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
- "features": {
- "ghcr.io/devcontainers/features/dotnet:2": {
- "version": "none",
- "dotnetRuntimeVersions": "8.0",
- "aspNetCoreRuntimeVersions": "8.0"
- },
- "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
- "preserve_apt_list": false,
- "packages": ["libfontconfig1"]
- },
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
- "dockerDashComposeVersion": "v2"
- },
- "ghcr.io/devcontainers/features/github-cli:1": {},
- "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
- },
- "hostRequirements": {
- "memory": "8gb",
- "cpus": 4
- }
-}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 063901c800..228d4a17c8 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,15 +1,15 @@
{
"name": "Development Jellyfin Server",
- "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
+ "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
+ "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
- "dotnetRuntimeVersions": "8.0",
- "aspNetCoreRuntimeVersions": "8.0"
+ "dotnetRuntimeVersions": "9.0",
+ "aspNetCoreRuntimeVersions": "9.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
diff --git a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh
similarity index 89%
rename from .devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
rename to .devcontainer/install-ffmpeg.sh
index c867ef538c..1e58e6ef44 100644
--- a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
+++ b/.devcontainer/install-ffmpeg.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-## configure the following for a manuall install of a specific version from the repo
+## configure the following for a manual install of a specific version from the repo
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
sudo apt update -y
-sudo apt install jellyfin-ffmpeg6 -y
+sudo apt install jellyfin-ffmpeg7 -y
diff --git a/.editorconfig b/.editorconfig
index b84e563efa..ab5d3d9dd1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -192,3 +192,341 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
+
+###############################
+# C# Analyzer Rules #
+###############################
+### ERROR #
+###########
+# error on SA1000: The keyword 'new' should be followed by a space
+dotnet_diagnostic.SA1000.severity = error
+
+# error on SA1001: Commas should not be preceded by whitespace
+dotnet_diagnostic.SA1001.severity = error
+
+# error on SA1106: Code should not contain empty statements
+dotnet_diagnostic.SA1106.severity = error
+
+# error on SA1107: Code should not contain multiple statements on one line
+dotnet_diagnostic.SA1107.severity = error
+
+# error on SA1028: Code should not contain trailing whitespace
+dotnet_diagnostic.SA1028.severity = error
+
+# error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line
+dotnet_diagnostic.SA1117.severity = error
+
+# error on SA1137: Elements should have the same indentation
+dotnet_diagnostic.SA1137.severity = error
+
+# error on SA1142: Refer to tuple fields by name
+dotnet_diagnostic.SA1142.severity = error
+
+# error on SA1210: Using directives should be ordered alphabetically by the namespaces
+dotnet_diagnostic.SA1210.severity = error
+
+# error on SA1316: Tuple element names should use correct casing
+dotnet_diagnostic.SA1316.severity = error
+
+# error on SA1414: Tuple types in signatures should have element names
+dotnet_diagnostic.SA1414.severity = error
+
+# disable warning SA1513: Closing brace should be followed by blank line
+dotnet_diagnostic.SA1513.severity = error
+
+# error on SA1518: File is required to end with a single newline character
+dotnet_diagnostic.SA1518.severity = error
+
+# error on SA1629: Documentation text should end with a period
+dotnet_diagnostic.SA1629.severity = error
+
+# error on CA1001: Types that own disposable fields should be disposable
+dotnet_diagnostic.CA1001.severity = error
+
+# error on CA1012: Abstract types should not have public constructors
+dotnet_diagnostic.CA1012.severity = error
+
+# error on CA1063: Implement IDisposable correctly
+dotnet_diagnostic.CA1063.severity = error
+
+# error on CA1305: Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = error
+
+# error on CA1307: Specify StringComparison for clarity
+dotnet_diagnostic.CA1307.severity = error
+
+# error on CA1309: Use ordinal StringComparison
+dotnet_diagnostic.CA1309.severity = error
+
+# error on CA1310: Specify StringComparison for correctness
+dotnet_diagnostic.CA1310.severity = error
+
+# error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
+dotnet_diagnostic.CA1513.severity = error
+
+# error on CA1725: Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = error
+
+# error on CA1725: Call async methods when in an async method
+dotnet_diagnostic.CA1727.severity = error
+
+# error on CA1813: Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = error
+
+# error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
+dotnet_diagnostic.CA1834.severity = error
+
+# error on CA1843: Do not use 'WaitAll' with a single task
+dotnet_diagnostic.CA1843.severity = error
+
+# error on CA1845: Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = error
+
+# error on CA1849: Call async methods when in an async method
+dotnet_diagnostic.CA1849.severity = error
+
+# error on CA1851: Possible multiple enumerations of IEnumerable collection
+dotnet_diagnostic.CA1851.severity = error
+
+# error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup
+dotnet_diagnostic.CA1854.severity = error
+
+# error on CA1860: Avoid using 'Enumerable.Any()' extension method
+dotnet_diagnostic.CA1860.severity = error
+
+# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
+dotnet_diagnostic.CA1862.severity = error
+
+# error on CA1863: Use 'CompositeFormat'
+dotnet_diagnostic.CA1863.severity = error
+
+# error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
+dotnet_diagnostic.CA1864.severity = error
+
+# error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
+dotnet_diagnostic.CA1865.severity = error
+dotnet_diagnostic.CA1866.severity = error
+dotnet_diagnostic.CA1867.severity = error
+
+# error on CA1868: Unnecessary call to 'Contains' for sets
+dotnet_diagnostic.CA1868.severity = error
+
+# error on CA1869: Cache and reuse 'JsonSerializerOptions' instances
+dotnet_diagnostic.CA1869.severity = error
+
+# error on CA1870: Use a cached 'SearchValues' instance
+dotnet_diagnostic.CA1870.severity = error
+
+# error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'
+dotnet_diagnostic.CA1871.severity = error
+
+# error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'
+dotnet_diagnostic.CA1872.severity = error
+
+# error on CA2016: Forward the CancellationToken parameter to methods that take one
+# or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token
+dotnet_diagnostic.CA2016.severity = error
+
+# error on CA2201: Exception type System.Exception is not sufficiently specific
+dotnet_diagnostic.CA2201.severity = error
+
+# error on CA2215: Dispose methods should call base class dispose
+dotnet_diagnostic.CA2215.severity = error
+
+# error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
+dotnet_diagnostic.CA2249.severity = error
+
+# error on CA2254: Template should be a static expression
+dotnet_diagnostic.CA2254.severity = error
+
+################
+### SUGGESTION #
+################
+# disable warning CA1014: Mark assemblies with CLSCompliantAttribute
+dotnet_diagnostic.CA1014.severity = suggestion
+
+# disable warning CA1024: Use properties where appropriate
+dotnet_diagnostic.CA1024.severity = suggestion
+
+# disable warning CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = suggestion
+
+# disable warning CA1032: Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = suggestion
+
+# disable warning CA1040: Avoid empty interfaces
+dotnet_diagnostic.CA1040.severity = suggestion
+
+# disable warning CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = suggestion
+
+# TODO: enable when false positives are fixed
+# disable warning CA1508: Avoid dead conditional code
+dotnet_diagnostic.CA1508.severity = suggestion
+
+# disable warning CA1515: Consider making public types internal
+dotnet_diagnostic.CA1515.severity = suggestion
+
+# disable warning CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = suggestion
+
+# disable warning CA1720: Identifiers should not contain type names
+dotnet_diagnostic.CA1720.severity = suggestion
+
+# disable warning CA1724: Type names should not match namespaces
+dotnet_diagnostic.CA1724.severity = suggestion
+
+# disable warning CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = suggestion
+
+# disable warning CA1812: internal class that is apparently never instantiated.
+# If so, remove the code from the assembly.
+# If this class is intended to contain only static members, make it static
+dotnet_diagnostic.CA1812.severity = suggestion
+
+# disable warning CA1822: Member does not access instance data and can be marked as static
+dotnet_diagnostic.CA1822.severity = suggestion
+
+# CA1859: Use concrete types when possible for improved performance
+dotnet_diagnostic.CA1859.severity = suggestion
+
+# TODO: Enable
+# CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
+dotnet_diagnostic.CA1861.severity = suggestion
+
+# disable warning CA2000: Dispose objects before losing scope
+dotnet_diagnostic.CA2000.severity = suggestion
+
+# disable warning CA2253: Named placeholders should not be numeric values
+dotnet_diagnostic.CA2253.severity = suggestion
+
+# disable warning CA5394: Do not use insecure randomness
+dotnet_diagnostic.CA5394.severity = suggestion
+
+# error on CA3003: Review code for file path injection vulnerabilities
+dotnet_diagnostic.CA3003.severity = suggestion
+
+# error on CA3006: Review code for process command injection vulnerabilities
+dotnet_diagnostic.CA3006.severity = suggestion
+
+###############
+### DISABLED #
+###############
+# disable warning SA1009: Closing parenthesis should be followed by a space.
+dotnet_diagnostic.SA1009.severity = none
+
+# disable warning SA1011: Closing square bracket should be followed by a space.
+dotnet_diagnostic.SA1011.severity = none
+
+# disable warning SA1101: Prefix local calls with 'this.'
+dotnet_diagnostic.SA1101.severity = none
+
+# disable warning SA1108: Block statements should not contain embedded comments
+dotnet_diagnostic.SA1108.severity = none
+
+# disable warning SA1118: Parameter must not span multiple lines.
+dotnet_diagnostic.SA1118.severity = none
+
+# disable warning SA1128:: Put constructor initializers on their own line
+dotnet_diagnostic.SA1128.severity = none
+
+# disable warning SA1130: Use lambda syntax
+dotnet_diagnostic.SA1130.severity = none
+
+# disable warning SA1200: 'using' directive must appear within a namespace declaration
+dotnet_diagnostic.SA1200.severity = none
+
+# disable warning SA1202: 'public' members must come before 'private' members
+dotnet_diagnostic.SA1202.severity = none
+
+# disable warning SA1204: Static members must appear before non-static members
+dotnet_diagnostic.SA1204.severity = none
+
+# disable warning SA1309: Fields must not begin with an underscore
+dotnet_diagnostic.SA1309.severity = none
+
+# disable warning SA1311: Static readonly fields should begin with upper-case letter
+dotnet_diagnostic.SA1311.severity = none
+
+# disable warning SA1413: Use trailing comma in multi-line initializers
+dotnet_diagnostic.SA1413.severity = none
+
+# disable warning SA1512: Single-line comments must not be followed by blank line
+dotnet_diagnostic.SA1512.severity = none
+
+# disable warning SA1515: Single-line comment should be preceded by blank line
+dotnet_diagnostic.SA1515.severity = none
+
+# disable warning SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = none
+
+# disable warning SA1601: Partial elements should be documented
+dotnet_diagnostic.SA1601.severity = none
+
+# disable warning SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1602.severity = none
+
+# disable warning SA1633: The file header is missing or not located at the top of the file
+dotnet_diagnostic.SA1633.severity = none
+
+# disable warning CA1054: Change the type of parameter url from string to System.Uri
+dotnet_diagnostic.CA1054.severity = none
+
+# disable warning CA1055: URI return values should not be strings
+dotnet_diagnostic.CA1055.severity = none
+
+# disable warning CA1056: URI properties should not be strings
+dotnet_diagnostic.CA1056.severity = none
+
+# disable warning CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
+# disable warning CA1308: Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = none
+
+# disable warning CA1848: Use the LoggerMessage delegates
+dotnet_diagnostic.CA1848.severity = none
+
+# disable warning CA2101: Specify marshaling for P/Invoke string arguments
+dotnet_diagnostic.CA2101.severity = none
+
+# disable warning CA2234: Pass System.Uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = none
+
+# error on RS0030: Do not used banned APIs
+dotnet_diagnostic.RS0030.severity = error
+
+# disable warning IDISP001: Dispose created
+dotnet_diagnostic.IDISP001.severity = suggestion
+
+# TODO: Enable when false positives are fixed
+# disable warning IDISP003: Dispose previous before re-assigning
+dotnet_diagnostic.IDISP003.severity = suggestion
+
+# disable warning IDISP004: Don't ignore created IDisposable
+dotnet_diagnostic.IDISP004.severity = suggestion
+
+# disable warning IDISP007: Don't dispose injected
+dotnet_diagnostic.IDISP007.severity = suggestion
+
+# disable warning IDISP008: Don't assign member with injected and created disposables
+dotnet_diagnostic.IDISP008.severity = suggestion
+
+[tests/**.{cs,vb}]
+# disable warning SA0001: XML comment analysis is disabled due to project configuration
+dotnet_diagnostic.SA0001.severity = none
+
+# disable warning CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1707.severity = none
+
+# disable warning CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = none
+
+# disable warning CA2234: Pass system uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = suggestion
+
+# disable warning xUnit1028: Test methods must have a supported return type.
+dotnet_diagnostic.xUnit1028.severity = none
+
+# CA1826: Do not use Enumerable methods on indexable collections
+dotnet_diagnostic.CA1826.severity = suggestion
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index b522412088..4f58c5bc50 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -14,7 +14,7 @@ body:
label: "This issue respects the following points:"
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
options:
- - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/).
+ - label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report.
required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.11+
+ - 10.10.0+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 1c55437a45..00f7e9e6d2 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -22,16 +22,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index c9d9f84493..07e61024ee 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -16,12 +16,17 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
+ - name: Setup .NET
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ with:
+ dotnet-version: '9.0.x'
+
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: abi-head
retention-days: 14
@@ -41,6 +46,11 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
+ - name: Setup .NET
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ with:
+ dotnet-version: '9.0.x'
+
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
@@ -55,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: abi-base
retention-days: 14
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 4633461ad7..e82988200d 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -21,18 +21,18 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@@ -55,18 +55,18 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-diff:
permissions:
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 54fb762e6b..ec78396db0 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -9,19 +9,20 @@ on:
pull_request:
env:
- SDK_VERSION: "8.0.x"
+ SDK_VERSION: "9.0.x"
jobs:
run-tests:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+ fail-fast: false
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -34,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # v5.3.11
+ uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 26b98f973d..1ab7ae029d 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -34,94 +34,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- check-backport:
- permissions:
- contents: read
-
- name: Check Backport
- if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
- runs-on: ubuntu-latest
- steps:
- - name: Notify as seen
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ github.event.comment.id }}
- reactions: eyes
-
- - name: Checkout the latest code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- fetch-depth: 0
-
- - name: Notify as running
- id: comment_running
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- issue-number: ${{ github.event.issue.number }}
- body: |
- Running backport tests...
-
- - name: Perform test backport
- id: run_tests
- run: |
- set +o errexit
- git config --global user.name "Jellyfin Bot"
- git config --global user.email "team@jellyfin.org"
- CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
- git checkout master
- git merge --no-ff ${CURRENT_BRANCH}
- MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
- git fetch --all
- CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
- stable_branch="Current stable release branch: ${CURRENT_STABLE}"
- echo ${stable_branch}
- echo ::set-output name=branch::${stable_branch}
- git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
- git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
- retcode=$?
- cat output.txt | grep -v 'hint:'
- output="$( grep -v 'hint:' output.txt )"
- output="${output//'%'/'%25'}"
- output="${output//$'\n'/'%0A'}"
- output="${output//$'\r'/'%0D'}"
- echo ::set-output name=output::$output
- exit ${retcode}
-
- - name: Notify with result success
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null && success() }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ steps.comment_running.outputs.comment-id }}
- body: |
- ${{ steps.run_tests.outputs.branch }}
- Output from `git cherry-pick`:
-
- ---
-
- ${{ steps.run_tests.outputs.output }}
- reactions: hooray
-
- - name: Notify with result failure
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null && failure() }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ steps.comment_running.outputs.comment-id }}
- body: |
- ${{ steps.run_tests.outputs.branch }}
- Output from `git cherry-pick`:
-
- ---
-
- ${{ steps.run_tests.outputs.output }}
- reactions: confused
-
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
@@ -132,7 +44,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml
index 5a1ca9f7a2..624ea564fb 100644
--- a/.github/workflows/issue-stale.yml
+++ b/.github/workflows/issue-stale.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index b72e552af0..3c5ba68f91 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -14,7 +14,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index 5d342b7f84..411ebf8290 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
+ uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
index d01b3f4a1f..7ce5b0fa61 100644
--- a/.github/workflows/pull-request-stale.yaml
+++ b/.github/workflows/pull-request-stale.yaml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3be946e446..e4205ce0b1 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,12 +1,13 @@
{
- "recommendations": [
+ "recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
- "ms-dotnettools.csdevkit"
- ],
- "unwantedRecommendations": [
+ "ms-dotnettools.csdevkit",
+ "alexcvzz.vscode-sqlite"
+ ],
+ "unwantedRecommendations": [
- ]
+ ]
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 7e50d4f0a4..d97d8de843 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a9deb1c4a2..ae1a2fd71e 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -192,6 +192,9 @@
- [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem)
+ - [Kenneth Cochran](https://github.com/kennethcochran)
+ - [benedikt257](https://github.com/benedikt257)
+ - [revam](https://github.com/revam)
# Emby Contributors
@@ -265,3 +268,4 @@
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
+ - [Martin Reuter](https://github.com/reuterma24)
diff --git a/Directory.Build.props b/Directory.Build.props
index 44a60ffb5c..31ae8bfbe4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,11 +3,11 @@
enable
- $(MSBuildThisFileDirectory)/jellyfin.ruleset
true
+ NU1902;NU1903
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5aadeb2541..999e4ba745 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,88 +4,87 @@
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
-
\ No newline at end of file
+
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 333d237a24..48338daf48 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -467,6 +467,14 @@ namespace Emby.Naming.Common
{
IsNamed = true
},
+
+ // Anime style expression
+ // "[Group][Series Name][21][1080p][FLAC][HASH]"
+ // "[Group] Series Name [04][BDRIP]"
+ new EpisodeExpression(@"(?:\[(?:[^\]]+)\]\s*)?(?\[[^\]]+\]|[^[\]]+)\s*\[(?[0-9]+)\]")
+ {
+ IsNamed = true
+ },
};
VideoExtraRules = new[]
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 7eb131575d..20b32f3a62 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,7 +6,7 @@
- net8.0
+ net9.0
false
true
true
@@ -36,7 +36,7 @@
Jellyfin Contributors
Jellyfin.Naming
- 10.10.0
+ 10.11.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
index d8fa417436..c955b8a0db 100644
--- a/Emby.Naming/TV/SeriesResolver.cs
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Naming.TV
///
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
- /// preserving namings like "S.H.O.W".
+ /// preserving names like "S.H.O.W".
///
[GeneratedRegex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))")]
private static partial Regex SeriesNameRegex();
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 55dbe393c7..645a74aea4 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
- net8.0
+ net9.0
false
true
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 9e98d5ce09..9bc3a0204b 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@@ -19,7 +20,7 @@ namespace Emby.Server.Implementations.AppBase
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly ConcurrentDictionary _configurations = new();
- private readonly object _configurationSyncLock = new();
+ private readonly Lock _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5292003f09..29967c6df5 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -83,7 +84,6 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
@@ -268,6 +268,11 @@ namespace Emby.Server.Implementations
public string ExpandVirtualPath(string path)
{
+ if (path is null)
+ {
+ return null;
+ }
+
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -492,10 +497,14 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -540,8 +549,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
-
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -579,9 +586,6 @@ namespace Emby.Server.Implementations
}
}
- ((SqliteItemRepository)Resolve()).Initialize();
- ((SqliteUserDataRepository)Resolve()).Initialize();
-
var localizationManager = (LocalizationManager)Resolve();
await localizationManager.LoadAll().ConfigureAwait(false);
@@ -607,7 +611,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
password = string.IsNullOrWhiteSpace(password) ? null : password;
- var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
+ var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
@@ -635,6 +639,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve();
BaseItem.LocalizationManager = Resolve();
BaseItem.ItemRepository = Resolve();
+ BaseItem.ChapterRepository = Resolve();
BaseItem.FileSystem = Resolve();
BaseItem.UserDataManager = Resolve();
BaseItem.ChannelManager = Resolve();
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 91791a1c82..a06f6e7fe9 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
deleted file mode 100644
index 8ed72c2082..0000000000
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ /dev/null
@@ -1,269 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Jellyfin.Extensions;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public abstract class BaseSqliteRepository : IDisposable
- {
- private bool _disposed = false;
- private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
- private SqliteConnection _writeConnection;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- protected BaseSqliteRepository(ILogger logger)
- {
- Logger = logger;
- }
-
- ///
- /// Gets or sets the path to the DB file.
- ///
- protected string DbFilePath { get; set; }
-
- ///
- /// Gets the logger.
- ///
- /// The logger.
- protected ILogger Logger { get; }
-
- ///
- /// Gets the cache size.
- ///
- /// The cache size or null.
- protected virtual int? CacheSize => null;
-
- ///
- /// Gets the locking mode. .
- ///
- protected virtual string LockingMode => "NORMAL";
-
- ///
- /// Gets the journal mode. .
- ///
- /// The journal mode.
- protected virtual string JournalMode => "WAL";
-
- ///
- /// Gets the journal size limit. .
- /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
- ///
- /// The journal size limit.
- protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
-
- ///
- /// Gets the page size.
- ///
- /// The page size or null.
- protected virtual int? PageSize => null;
-
- ///
- /// Gets the temp store mode.
- ///
- /// The temp store mode.
- ///
- protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
-
- ///
- /// Gets the synchronous mode.
- ///
- /// The synchronous mode or null.
- ///
- protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
-
- public virtual void Initialize()
- {
- // Configuration and pragmas can affect VACUUM so it needs to be last.
- using (var connection = GetConnection())
- {
- connection.Execute("VACUUM");
- }
- }
-
- protected ManagedConnection GetConnection(bool readOnly = false)
- {
- if (!readOnly)
- {
- _writeLock.Wait();
- if (_writeConnection is not null)
- {
- return new ManagedConnection(_writeConnection, _writeLock);
- }
-
- var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
- writeConnection.Open();
-
- if (CacheSize.HasValue)
- {
- writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
- }
-
- var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
- connection.Open();
-
- if (CacheSize.HasValue)
- {
- connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- connection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- connection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- connection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- connection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(connection, null);
- }
-
- public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
- return command;
- }
-
- protected bool TableExists(ManagedConnection connection, string name)
- {
- using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
- foreach (var row in statement.ExecuteQuery())
- {
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
-
- protected List GetColumnNames(ManagedConnection connection, string table)
- {
- var columnNames = new List();
-
- foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
- {
- if (row.TryGetString(1, out var columnName))
- {
- columnNames.Add(columnName);
- }
- }
-
- return columnNames;
- }
-
- protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List existingColumnNames)
- {
- if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
- }
-
- protected void CheckDisposed()
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- }
-
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Releases unmanaged and - optionally - managed resources.
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- _writeLock.Wait();
- try
- {
- _writeConnection.Dispose();
- }
- finally
- {
- _writeLock.Release();
- }
-
- _writeLock.Dispose();
- }
-
- _writeConnection = null;
- _writeLock = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 4516b89dc2..7ea863d769 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,10 +1,13 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
@@ -13,20 +16,24 @@ namespace Emby.Server.Implementations.Data
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
+ private readonly IDbContextFactory _dbProvider;
- public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger)
+ public CleanDatabaseScheduledTask(
+ ILibraryManager libraryManager,
+ ILogger logger,
+ IDbContextFactory dbProvider)
{
_libraryManager = libraryManager;
_logger = logger;
+ _dbProvider = dbProvider;
}
- public Task Run(IProgress progress, CancellationToken cancellationToken)
+ public async Task Run(IProgress progress, CancellationToken cancellationToken)
{
- CleanDeadItems(cancellationToken, progress);
- return Task.CompletedTask;
+ await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
- private void CleanDeadItems(CancellationToken cancellationToken, IProgress progress)
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
@@ -34,7 +41,7 @@ namespace Emby.Server.Implementations.Data
});
var numComplete = 0;
- var numItems = itemIds.Count;
+ var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
@@ -60,6 +67,17 @@ namespace Emby.Server.Implementations.Data
progress.Report(percent * 100);
}
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
+ {
+ await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
new file mode 100644
index 0000000000..82c0a8b6c5
--- /dev/null
+++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
@@ -0,0 +1,64 @@
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Threading.Channels;
+using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Playlists;
+
+namespace Emby.Server.Implementations.Data;
+
+///
+public class ItemTypeLookup : IItemTypeLookup
+{
+ ///
+ public IReadOnlyList MusicGenreTypes { get; } = [
+ typeof(Audio).FullName!,
+ typeof(MusicVideo).FullName!,
+ typeof(MusicAlbum).FullName!,
+ typeof(MusicArtist).FullName!,
+ ];
+
+ ///
+ public IReadOnlyDictionary BaseItemKindNames { get; } = new Dictionary()
+ {
+ { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
+ { BaseItemKind.Audio, typeof(Audio).FullName! },
+ { BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
+ { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
+ { BaseItemKind.Book, typeof(Book).FullName! },
+ { BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
+ { BaseItemKind.Channel, typeof(Channel).FullName! },
+ { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
+ { BaseItemKind.Episode, typeof(Episode).FullName! },
+ { BaseItemKind.Folder, typeof(Folder).FullName! },
+ { BaseItemKind.Genre, typeof(Genre).FullName! },
+ { BaseItemKind.Movie, typeof(Movie).FullName! },
+ { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
+ { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
+ { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
+ { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
+ { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
+ { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
+ { BaseItemKind.Person, typeof(Person).FullName! },
+ { BaseItemKind.Photo, typeof(Photo).FullName! },
+ { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
+ { BaseItemKind.Playlist, typeof(Playlist).FullName! },
+ { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
+ { BaseItemKind.Season, typeof(Season).FullName! },
+ { BaseItemKind.Series, typeof(Series).FullName! },
+ { BaseItemKind.Studio, typeof(Studio).FullName! },
+ { BaseItemKind.Trailer, typeof(Trailer).FullName! },
+ { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
+ { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
+ { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
+ { BaseItemKind.UserView, typeof(UserView).FullName! },
+ { BaseItemKind.Video, typeof(Video).FullName! },
+ { BaseItemKind.Year, typeof(Year).FullName! }
+ }.ToFrozenDictionary();
+}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
deleted file mode 100644
index 860950b303..0000000000
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Microsoft.Data.Sqlite;
-
-namespace Emby.Server.Implementations.Data;
-
-public sealed class ManagedConnection : IDisposable
-{
- private readonly SemaphoreSlim? _writeLock;
-
- private SqliteConnection _db;
-
- private bool _disposed = false;
-
- public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
- {
- _db = db;
- _writeLock = writeLock;
- }
-
- public SqliteTransaction BeginTransaction()
- => _db.BeginTransaction();
-
- public SqliteCommand CreateCommand()
- => _db.CreateCommand();
-
- public void Execute(string commandText)
- => _db.Execute(commandText);
-
- public SqliteCommand PrepareStatement(string sql)
- => _db.PrepareStatement(sql);
-
- public IEnumerable Query(string commandText)
- => _db.Query(commandText);
-
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- if (_writeLock is null)
- {
- // Read connections are managed with an internal pool
- _db.Dispose();
- }
- else
- {
- // Write lock is managed by BaseSqliteRepository
- // Don't dispose here
- _writeLock.Release();
- }
-
- _db = null!;
-
- _disposed = true;
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 25ef57d271..0efef4dedc 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data
return false;
}
- result = reader.GetGuid(index);
- return true;
+ try
+ {
+ result = reader.GetGuid(index);
+ return true;
+ }
+ catch
+ {
+ result = Guid.Empty;
+ return false;
+ }
}
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
deleted file mode 100644
index 3477194cf7..0000000000
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ /dev/null
@@ -1,5971 +0,0 @@
-#nullable disable
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using Emby.Server.Implementations.Playlists;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- ///
- /// Class SQLiteItemRepository.
- ///
- public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
- {
- private const string FromText = " from TypedBaseItems A";
- private const string ChaptersTableName = "Chapters2";
-
- private const string SaveItemCommandText =
- @"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
- private readonly IServerConfigurationManager _config;
- private readonly IServerApplicationHost _appHost;
- private readonly ILocalizationManager _localization;
- // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method
- private readonly IImageProcessor _imageProcessor;
-
- private readonly TypeMapper _typeMapper;
- private readonly JsonSerializerOptions _jsonOptions;
-
- private readonly ItemFields[] _allItemFields = Enum.GetValues();
-
- private static readonly string[] _retrieveItemColumns =
- {
- "type",
- "data",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "Name",
- "Path",
- "PremiereDate",
- "Overview",
- "ParentIndexNumber",
- "ProductionYear",
- "OfficialRating",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "guid",
- "Genres",
- "ParentId",
- "Audio",
- "ExternalServiceId",
- "IsInMixedFolder",
- "DateLastSaved",
- "LockedFields",
- "Studios",
- "Tags",
- "TrailerTypes",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "LUFS",
- "NormalizationGain",
- "CriticRating",
- "IsVirtualItem",
- "SeriesName",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "PresentationUniqueKey",
- "InheritedParentalRatingValue",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
-
- private static readonly string[] _mediaStreamSaveColumns =
- {
- "ItemId",
- "StreamIndex",
- "StreamType",
- "Codec",
- "Language",
- "ChannelLayout",
- "Profile",
- "AspectRatio",
- "Path",
- "IsInterlaced",
- "BitRate",
- "Channels",
- "SampleRate",
- "IsDefault",
- "IsForced",
- "IsExternal",
- "Height",
- "Width",
- "AverageFrameRate",
- "RealFrameRate",
- "Level",
- "PixelFormat",
- "BitDepth",
- "IsAnamorphic",
- "RefFrames",
- "CodecTag",
- "Comment",
- "NalLengthSize",
- "IsAvc",
- "Title",
- "TimeBase",
- "CodecTimeBase",
- "ColorPrimaries",
- "ColorSpace",
- "ColorTransfer",
- "DvVersionMajor",
- "DvVersionMinor",
- "DvProfile",
- "DvLevel",
- "RpuPresentFlag",
- "ElPresentFlag",
- "BlPresentFlag",
- "DvBlSignalCompatibilityId",
- "IsHearingImpaired",
- "Rotation"
- };
-
- private static readonly string _mediaStreamSaveColumnsInsertQuery =
- $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
-
- private static readonly string _mediaStreamSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
-
- private static readonly string[] _mediaAttachmentSaveColumns =
- {
- "ItemId",
- "AttachmentIndex",
- "Codec",
- "CodecTag",
- "Comment",
- "Filename",
- "MIMEType"
- };
-
- private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
-
- private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix();
-
- private static readonly BaseItemKind[] _programTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvProgram,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicArtist,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _serviceTypes = new[]
- {
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _startDateTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.LiveTvProgram
- };
-
- private static readonly BaseItemKind[] _seriesTypes = new[]
- {
- BaseItemKind.Book,
- BaseItemKind.AudioBook,
- BaseItemKind.Episode,
- BaseItemKind.Season
- };
-
- private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _artistsTypes = new[]
- {
- BaseItemKind.Audio,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicVideo,
- BaseItemKind.AudioBook
- };
-
- private static readonly Dictionary _baseItemKindNames = new()
- {
- { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
- { BaseItemKind.Audio, typeof(Audio).FullName },
- { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
- { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
- { BaseItemKind.Book, typeof(Book).FullName },
- { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
- { BaseItemKind.Channel, typeof(Channel).FullName },
- { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
- { BaseItemKind.Episode, typeof(Episode).FullName },
- { BaseItemKind.Folder, typeof(Folder).FullName },
- { BaseItemKind.Genre, typeof(Genre).FullName },
- { BaseItemKind.Movie, typeof(Movie).FullName },
- { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
- { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
- { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
- { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
- { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
- { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
- { BaseItemKind.Person, typeof(Person).FullName },
- { BaseItemKind.Photo, typeof(Photo).FullName },
- { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
- { BaseItemKind.Playlist, typeof(Playlist).FullName },
- { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
- { BaseItemKind.Season, typeof(Season).FullName },
- { BaseItemKind.Series, typeof(Series).FullName },
- { BaseItemKind.Studio, typeof(Studio).FullName },
- { BaseItemKind.Trailer, typeof(Trailer).FullName },
- { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
- { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
- { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
- { BaseItemKind.UserView, typeof(UserView).FullName },
- { BaseItemKind.Video, typeof(Video).FullName },
- { BaseItemKind.Year, typeof(Year).FullName }
- };
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// config is null.
- public SqliteItemRepository(
- IServerConfigurationManager config,
- IServerApplicationHost appHost,
- ILogger logger,
- ILocalizationManager localization,
- IImageProcessor imageProcessor,
- IConfiguration configuration)
- : base(logger)
- {
- _config = config;
- _appHost = appHost;
- _localization = localization;
- _imageProcessor = imageProcessor;
-
- _typeMapper = new TypeMapper();
- _jsonOptions = JsonDefaults.Options;
-
- DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
-
- CacheSize = configuration.GetSqliteCacheSize();
- }
-
- ///
- protected override int? CacheSize { get; }
-
- ///
- protected override TempStoreMode TempStore => TempStoreMode.Memory;
-
- ///
- /// Opens the connection to the database.
- ///
- public override void Initialize()
- {
- base.Initialize();
-
- const string CreateMediaStreamsTableCommand
- = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
-
- const string CreateMediaAttachmentsTableCommand
- = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
-
- string[] queries =
- {
- "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
-
- "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
- "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
- "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
-
- "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
-
- "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
-
- "drop index if exists idxPeopleItemId",
- "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
- "create index if not exists idxPeopleName on People(Name)",
-
- "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
-
- CreateMediaStreamsTableCommand,
- CreateMediaAttachmentsTableCommand,
-
- "pragma shrink_memory"
- };
-
- string[] postQueries =
- {
- "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
- "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
-
- "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
- "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
- "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
-
- // covering index
- "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
-
- // series
- "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
-
- // series counts
- // seriesdateplayed sort order
- "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
-
- // live tv programs
- "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
-
- // covering index for getitemvalues
- "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
-
- // used by movie suggestions
- "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
- "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
-
- // latest items
- "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
- "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
-
- // resume
- "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
-
- // items by name
- "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
- "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
-
- // Used to update inherited tags
- "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
-
- "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)",
- "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)"
- };
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- connection.Execute(string.Join(';', queries));
-
- var existingColumnNames = GetColumnNames(connection, "AncestorIds");
- AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
-
- AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "ItemValues");
- AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, ChaptersTableName);
- AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "MediaStreams");
- AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames);
-
- connection.Execute(string.Join(';', postQueries));
-
- transaction.Commit();
- }
- }
-
- ///
- public void SaveImages(BaseItem item)
- {
- ArgumentNullException.ThrowIfNull(item);
-
- CheckDisposed();
-
- var images = SerializeImages(item.ImageInfos);
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
- saveImagesStatement.TryBind("@Id", item.Id);
- saveImagesStatement.TryBind("@Images", images);
-
- saveImagesStatement.ExecuteNonQuery();
- transaction.Commit();
- }
-
- ///
- /// Saves the items.
- ///
- /// The items.
- /// The cancellation token.
- ///
- /// or is null.
- ///
- public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(items);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- CheckDisposed();
-
- var itemsLen = items.Count;
- var tuples = new ValueTuple, BaseItem, string, List>[itemsLen];
- for (int i = 0; i < itemsLen; i++)
- {
- var item = items[i];
- var ancestorIds = item.SupportsAncestors ?
- item.GetAncestorIds().Distinct().ToList() :
- null;
-
- var topParent = item.GetTopParent();
-
- var userdataKey = item.GetUserDataKeys().FirstOrDefault();
- var inheritedTags = item.GetInheritedTags();
-
- tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
- }
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- SaveItemsInTransaction(connection, tuples);
- transaction.Commit();
- }
-
- private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples)
- {
- using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
- using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
- {
- var requiresReset = false;
- foreach (var tuple in tuples)
- {
- if (requiresReset)
- {
- saveItemStatement.Parameters.Clear();
- deleteAncestorsStatement.Parameters.Clear();
- }
-
- var item = tuple.Item;
- var topParent = tuple.TopParent;
- var userDataKey = tuple.UserDataKey;
-
- SaveItem(item, topParent, userDataKey, saveItemStatement);
-
- var inheritedTags = tuple.InheritedTags;
-
- if (item.SupportsAncestors)
- {
- UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement);
- }
-
- UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db);
-
- requiresReset = true;
- }
- }
- }
-
- private string GetPathToSave(string path)
- {
- if (path is null)
- {
- return null;
- }
-
- return _appHost.ReverseVirtualPath(path);
- }
-
- private string RestorePath(string path)
- {
- return _appHost.ExpandVirtualPath(path);
- }
-
- private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
- {
- Type type = item.GetType();
-
- saveItemStatement.TryBind("@guid", item.Id);
- saveItemStatement.TryBind("@type", type.FullName);
-
- if (TypeRequiresDeserialization(type))
- {
- saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
- }
- else
- {
- saveItemStatement.TryBindNull("@data");
- }
-
- saveItemStatement.TryBind("@Path", GetPathToSave(item.Path));
-
- if (item is IHasStartDate hasStartDate)
- {
- saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate);
- }
- else
- {
- saveItemStatement.TryBindNull("@StartDate");
- }
-
- if (item.EndDate.HasValue)
- {
- saveItemStatement.TryBind("@EndDate", item.EndDate.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@EndDate");
- }
-
- saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
-
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie);
- saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries);
- saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle);
- saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat);
- }
- else
- {
- saveItemStatement.TryBindNull("@IsMovie");
- saveItemStatement.TryBindNull("@IsSeries");
- saveItemStatement.TryBindNull("@EpisodeTitle");
- saveItemStatement.TryBindNull("@IsRepeat");
- }
-
- saveItemStatement.TryBind("@CommunityRating", item.CommunityRating);
- saveItemStatement.TryBind("@CustomRating", item.CustomRating);
- saveItemStatement.TryBind("@IndexNumber", item.IndexNumber);
- saveItemStatement.TryBind("@IsLocked", item.IsLocked);
- saveItemStatement.TryBind("@Name", item.Name);
- saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
- saveItemStatement.TryBind("@Overview", item.Overview);
- saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
- saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
- saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
-
- var parentId = item.ParentId;
- if (parentId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@ParentId");
- }
- else
- {
- saveItemStatement.TryBind("@ParentId", parentId);
- }
-
- if (item.Genres.Length > 0)
- {
- saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
- }
- else
- {
- saveItemStatement.TryBindNull("@Genres");
- }
-
- saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
-
- saveItemStatement.TryBind("@SortName", item.SortName);
-
- saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName);
-
- saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks);
- saveItemStatement.TryBind("@Size", item.Size);
-
- saveItemStatement.TryBind("@DateCreated", item.DateCreated);
- saveItemStatement.TryBind("@DateModified", item.DateModified);
-
- saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage);
- saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode);
-
- if (item.Width > 0)
- {
- saveItemStatement.TryBind("@Width", item.Width);
- }
- else
- {
- saveItemStatement.TryBindNull("@Width");
- }
-
- if (item.Height > 0)
- {
- saveItemStatement.TryBind("@Height", item.Height);
- }
- else
- {
- saveItemStatement.TryBindNull("@Height");
- }
-
- if (item.DateLastRefreshed != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastRefreshed");
- }
-
- if (item.DateLastSaved != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastSaved");
- }
-
- saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder);
-
- if (item.LockedFields.Length > 0)
- {
- saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
- }
- else
- {
- saveItemStatement.TryBindNull("@LockedFields");
- }
-
- if (item.Studios.Length > 0)
- {
- saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
- }
- else
- {
- saveItemStatement.TryBindNull("@Studios");
- }
-
- if (item.Audio.HasValue)
- {
- saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@Audio");
- }
-
- if (item is LiveTvChannel liveTvChannel)
- {
- saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName);
- }
- else
- {
- saveItemStatement.TryBindNull("@ExternalServiceId");
- }
-
- if (item.Tags.Length > 0)
- {
- saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
- }
- else
- {
- saveItemStatement.TryBindNull("@Tags");
- }
-
- saveItemStatement.TryBind("@IsFolder", item.IsFolder);
-
- saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString());
-
- if (topParent is null)
- {
- saveItemStatement.TryBindNull("@TopParentId");
- }
- else
- {
- saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture));
- }
-
- if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
- {
- saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
- }
- else
- {
- saveItemStatement.TryBindNull("@TrailerTypes");
- }
-
- saveItemStatement.TryBind("@CriticRating", item.CriticRating);
-
- if (string.IsNullOrWhiteSpace(item.Name))
- {
- saveItemStatement.TryBindNull("@CleanName");
- }
- else
- {
- saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name));
- }
-
- saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey);
- saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle);
-
- if (item is Video video)
- {
- saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId);
- }
- else
- {
- saveItemStatement.TryBindNull("@PrimaryVersionId");
- }
-
- if (item is Folder folder && folder.DateLastMediaAdded.HasValue)
- {
- saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastMediaAdded");
- }
-
- saveItemStatement.TryBind("@Album", item.Album);
- saveItemStatement.TryBind("@LUFS", item.LUFS);
- saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
- saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
-
- if (item is IHasSeries hasSeriesName)
- {
- saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesName");
- }
-
- if (string.IsNullOrWhiteSpace(userDataKey))
- {
- saveItemStatement.TryBindNull("@UserDataKey");
- }
- else
- {
- saveItemStatement.TryBind("@UserDataKey", userDataKey);
- }
-
- if (item is Episode episode)
- {
- saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
-
- var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
-
- saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeasonName");
- saveItemStatement.TryBindNull("@SeasonId");
- }
-
- if (item is IHasSeries hasSeries)
- {
- var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
-
- saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
- saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesId");
- saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey");
- }
-
- saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
- saveItemStatement.TryBind("@Tagline", item.Tagline);
-
- saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
- saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
-
- if (item.ProductionLocations.Length > 0)
- {
- saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
- }
- else
- {
- saveItemStatement.TryBindNull("@ProductionLocations");
- }
-
- if (item.ExtraIds.Length > 0)
- {
- saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraIds");
- }
-
- saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate);
- if (item.ExtraType.HasValue)
- {
- saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraType");
- }
-
- string artists = null;
- if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
- {
- artists = string.Join('|', hasArtists.Artists);
- }
-
- saveItemStatement.TryBind("@Artists", artists);
-
- string albumArtists = null;
- if (item is IHasAlbumArtist hasAlbumArtists
- && hasAlbumArtists.AlbumArtists.Count > 0)
- {
- albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
- }
-
- saveItemStatement.TryBind("@AlbumArtists", albumArtists);
- saveItemStatement.TryBind("@ExternalId", item.ExternalId);
-
- if (item is LiveTvProgram program)
- {
- saveItemStatement.TryBind("@ShowId", program.ShowId);
- }
- else
- {
- saveItemStatement.TryBindNull("@ShowId");
- }
-
- Guid ownerId = item.OwnerId;
- if (ownerId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@OwnerId");
- }
- else
- {
- saveItemStatement.TryBind("@OwnerId", ownerId);
- }
-
- saveItemStatement.ExecuteNonQuery();
- }
-
- internal static string SerializeProviderIds(Dictionary providerIds)
- {
- StringBuilder str = new StringBuilder();
- foreach (var i in providerIds)
- {
- // Ideally we shouldn't need this IsNullOrWhiteSpace check,
- // but we're seeing some cases of bad data slip through
- if (string.IsNullOrWhiteSpace(i.Value))
- {
- continue;
- }
-
- str.Append(i.Key)
- .Append('=')
- .Append(i.Value)
- .Append('|');
- }
-
- if (str.Length == 0)
- {
- return null;
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal static void DeserializeProviderIds(string value, IHasProviderIds item)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return;
- }
-
- foreach (var part in value.SpanSplit('|'))
- {
- var providerDelimiterIndex = part.IndexOf('=');
- // Don't let empty values through
- if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1)
- {
- item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString());
- }
- }
- }
-
- internal string SerializeImages(ItemImageInfo[] images)
- {
- if (images.Length == 0)
- {
- return null;
- }
-
- StringBuilder str = new StringBuilder();
- foreach (var i in images)
- {
- if (string.IsNullOrWhiteSpace(i.Path))
- {
- continue;
- }
-
- AppendItemImageInfo(str, i);
- str.Append('|');
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal ItemImageInfo[] DeserializeImages(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return Array.Empty();
- }
-
- // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
- var valueSpan = value.AsSpan();
- var count = valueSpan.Count('|') + 1;
-
- var position = 0;
- var result = new ItemImageInfo[count];
- foreach (var part in valueSpan.Split('|'))
- {
- var image = ItemImageInfoFromValueString(part);
-
- if (image is not null)
- {
- result[position++] = image;
- }
- }
-
- if (position == count)
- {
- return result;
- }
-
- if (position == 0)
- {
- return Array.Empty();
- }
-
- // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
- return result[..position];
- }
-
- private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
- {
- const char Delimiter = '*';
-
- var path = image.Path ?? string.Empty;
-
- bldr.Append(GetPathToSave(path))
- .Append(Delimiter)
- .Append(image.DateModified.Ticks)
- .Append(Delimiter)
- .Append(image.Type)
- .Append(Delimiter)
- .Append(image.Width)
- .Append(Delimiter)
- .Append(image.Height);
-
- var hash = image.BlurHash;
- if (!string.IsNullOrEmpty(hash))
- {
- bldr.Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
- }
- }
-
- internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value)
- {
- const char Delimiter = '*';
-
- var nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan path = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan dateModified = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan imageType = value[..nextSegment];
-
- var image = new ItemImageInfo
- {
- Path = RestorePath(path.ToString())
- };
-
- if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
- && ticks >= DateTime.MinValue.Ticks
- && ticks <= DateTime.MaxValue.Ticks)
- {
- image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
- }
- else
- {
- return null;
- }
-
- if (Enum.TryParse(imageType, true, out ImageType type))
- {
- image.Type = type;
- }
- else
- {
- return null;
- }
-
- // Optional parameters: width*height*blurhash
- if (nextSegment + 1 < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1 || nextSegment == value.Length)
- {
- return image;
- }
-
- ReadOnlySpan widthSpan = value[..nextSegment];
-
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan heightSpan = value[..nextSegment];
-
- if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
- && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
- {
- image.Width = width;
- image.Height = height;
- }
-
- if (nextSegment < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- var length = value.Length;
-
- Span blurHashSpan = stackalloc char[length];
- for (int i = 0; i < length; i++)
- {
- var c = value[i];
- blurHashSpan[i] = c switch
- {
- '/' => Delimiter,
- '\\' => '|',
- _ => c
- };
- }
-
- image.BlurHash = new string(blurHashSpan);
- }
- }
-
- return image;
- }
-
- ///
- /// Internal retrieve from items or users table.
- ///
- /// The id.
- /// BaseItem.
- /// is null.
- /// is .
- public BaseItem RetrieveItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty", nameof(id));
- }
-
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
- {
- statement.TryBind("@guid", id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetItem(row, new InternalItemsQuery());
- }
- }
-
- return null;
- }
-
- private bool TypeRequiresDeserialization(Type type)
- {
- if (_config.Configuration.SkipDeserializationForBasicTypes)
- {
- if (type == typeof(Channel)
- || type == typeof(UserRootFolder))
- {
- return false;
- }
- }
-
- return type != typeof(Season)
- && type != typeof(MusicArtist)
- && type != typeof(Person)
- && type != typeof(MusicGenre)
- && type != typeof(Genre)
- && type != typeof(Studio)
- && type != typeof(PlaylistsFolder)
- && type != typeof(PhotoAlbum)
- && type != typeof(Year)
- && type != typeof(Book)
- && type != typeof(LiveTvProgram)
- && type != typeof(AudioBook)
- && type != typeof(MusicAlbum);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
- {
- return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
- {
- var typeString = reader.GetString(0);
-
- var type = _typeMapper.GetType(typeString);
-
- if (type is null)
- {
- return null;
- }
-
- BaseItem item = null;
-
- if (TypeRequiresDeserialization(type) && !skipDeserialization)
- {
- try
- {
- item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem;
- }
- catch (JsonException ex)
- {
- Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1));
- }
- }
-
- if (item is null)
- {
- try
- {
- item = Activator.CreateInstance(type) as BaseItem;
- }
- catch
- {
- }
- }
-
- if (item is null)
- {
- return null;
- }
-
- var index = 2;
-
- if (queryHasStartDate)
- {
- if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate))
- {
- hasStartDate.StartDate = startDate;
- }
-
- index++;
- }
-
- if (reader.TryReadDateTime(index++, out var endDate))
- {
- item.EndDate = endDate;
- }
-
- if (reader.TryGetGuid(index, out var guid))
- {
- item.ChannelId = guid;
- }
-
- index++;
-
- if (enableProgramAttributes)
- {
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- if (reader.TryGetBoolean(index++, out var isMovie))
- {
- hasProgramAttributes.IsMovie = isMovie;
- }
-
- if (reader.TryGetBoolean(index++, out var isSeries))
- {
- hasProgramAttributes.IsSeries = isSeries;
- }
-
- if (reader.TryGetString(index++, out var episodeTitle))
- {
- hasProgramAttributes.EpisodeTitle = episodeTitle;
- }
-
- if (reader.TryGetBoolean(index++, out var isRepeat))
- {
- hasProgramAttributes.IsRepeat = isRepeat;
- }
- }
- else
- {
- index += 4;
- }
- }
-
- if (reader.TryGetSingle(index++, out var communityRating))
- {
- item.CommunityRating = communityRating;
- }
-
- if (HasField(query, ItemFields.CustomRating))
- {
- if (reader.TryGetString(index++, out var customRating))
- {
- item.CustomRating = customRating;
- }
- }
-
- if (reader.TryGetInt32(index++, out var indexNumber))
- {
- item.IndexNumber = indexNumber;
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetBoolean(index++, out var isLocked))
- {
- item.IsLocked = isLocked;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataLanguage))
- {
- item.PreferredMetadataLanguage = preferredMetadataLanguage;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
- {
- item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
- }
- }
-
- if (HasField(query, ItemFields.Width))
- {
- if (reader.TryGetInt32(index++, out var width))
- {
- item.Width = width;
- }
- }
-
- if (HasField(query, ItemFields.Height))
- {
- if (reader.TryGetInt32(index++, out var height))
- {
- item.Height = height;
- }
- }
-
- if (HasField(query, ItemFields.DateLastRefreshed))
- {
- if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
- {
- item.DateLastRefreshed = dateLastRefreshed;
- }
- }
-
- if (reader.TryGetString(index++, out var name))
- {
- item.Name = name;
- }
-
- if (reader.TryGetString(index++, out var restorePath))
- {
- item.Path = RestorePath(restorePath);
- }
-
- if (reader.TryReadDateTime(index++, out var premiereDate))
- {
- item.PremiereDate = premiereDate;
- }
-
- if (HasField(query, ItemFields.Overview))
- {
- if (reader.TryGetString(index++, out var overview))
- {
- item.Overview = overview;
- }
- }
-
- if (reader.TryGetInt32(index++, out var parentIndexNumber))
- {
- item.ParentIndexNumber = parentIndexNumber;
- }
-
- if (reader.TryGetInt32(index++, out var productionYear))
- {
- item.ProductionYear = productionYear;
- }
-
- if (reader.TryGetString(index++, out var officialRating))
- {
- item.OfficialRating = officialRating;
- }
-
- if (HasField(query, ItemFields.SortName))
- {
- if (reader.TryGetString(index++, out var forcedSortName))
- {
- item.ForcedSortName = forcedSortName;
- }
- }
-
- if (reader.TryGetInt64(index++, out var runTimeTicks))
- {
- item.RunTimeTicks = runTimeTicks;
- }
-
- if (reader.TryGetInt64(index++, out var size))
- {
- item.Size = size;
- }
-
- if (HasField(query, ItemFields.DateCreated))
- {
- if (reader.TryReadDateTime(index++, out var dateCreated))
- {
- item.DateCreated = dateCreated;
- }
- }
-
- if (reader.TryReadDateTime(index++, out var dateModified))
- {
- item.DateModified = dateModified;
- }
-
- item.Id = reader.GetGuid(index++);
-
- if (HasField(query, ItemFields.Genres))
- {
- if (reader.TryGetString(index++, out var genres))
- {
- item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (reader.TryGetGuid(index++, out var parentId))
- {
- item.ParentId = parentId;
- }
-
- if (reader.TryGetString(index++, out var audioString))
- {
- if (Enum.TryParse(audioString, true, out ProgramAudio audio))
- {
- item.Audio = audio;
- }
- }
-
- // TODO: Even if not needed by apps, the server needs it internally
- // But get this excluded from contexts where it is not needed
- if (hasServiceName)
- {
- if (item is LiveTvChannel liveTvChannel)
- {
- if (reader.TryGetString(index, out var serviceName))
- {
- liveTvChannel.ServiceName = serviceName;
- }
- }
-
- index++;
- }
-
- if (reader.TryGetBoolean(index++, out var isInMixedFolder))
- {
- item.IsInMixedFolder = isInMixedFolder;
- }
-
- if (HasField(query, ItemFields.DateLastSaved))
- {
- if (reader.TryReadDateTime(index++, out var dateLastSaved))
- {
- item.DateLastSaved = dateLastSaved;
- }
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetString(index++, out var lockedFields))
- {
- List fields = null;
- foreach (var i in lockedFields.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- (fields ??= new List()).Add(parsedValue);
- }
- }
-
- item.LockedFields = fields?.ToArray() ?? Array.Empty();
- }
- }
-
- if (HasField(query, ItemFields.Studios))
- {
- if (reader.TryGetString(index++, out var studios))
- {
- item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.Tags))
- {
- if (reader.TryGetString(index++, out var tags))
- {
- item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (hasTrailerTypes)
- {
- if (item is Trailer trailer)
- {
- if (reader.TryGetString(index, out var trailerTypes))
- {
- List types = null;
- foreach (var i in trailerTypes.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- (types ??= new List()).Add(parsedValue);
- }
- }
-
- trailer.TrailerTypes = types?.ToArray() ?? Array.Empty();
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.OriginalTitle))
- {
- if (reader.TryGetString(index++, out var originalTitle))
- {
- item.OriginalTitle = originalTitle;
- }
- }
-
- if (item is Video video)
- {
- if (reader.TryGetString(index, out var primaryVersionId))
- {
- video.PrimaryVersionId = primaryVersionId;
- }
- }
-
- index++;
-
- if (HasField(query, ItemFields.DateLastMediaAdded))
- {
- if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded))
- {
- folder.DateLastMediaAdded = dateLastMediaAdded;
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var album))
- {
- item.Album = album;
- }
-
- if (reader.TryGetSingle(index++, out var lUFS))
- {
- item.LUFS = lUFS;
- }
-
- if (reader.TryGetSingle(index++, out var normalizationGain))
- {
- item.NormalizationGain = normalizationGain;
- }
-
- if (reader.TryGetSingle(index++, out var criticRating))
- {
- item.CriticRating = criticRating;
- }
-
- if (reader.TryGetBoolean(index++, out var isVirtualItem))
- {
- item.IsVirtualItem = isVirtualItem;
- }
-
- if (item is IHasSeries hasSeriesName)
- {
- if (reader.TryGetString(index, out var seriesName))
- {
- hasSeriesName.SeriesName = seriesName;
- }
- }
-
- index++;
-
- if (hasEpisodeAttributes)
- {
- if (item is Episode episode)
- {
- if (reader.TryGetString(index, out var seasonName))
- {
- episode.SeasonName = seasonName;
- }
-
- index++;
- if (reader.TryGetGuid(index, out var seasonId))
- {
- episode.SeasonId = seasonId;
- }
- }
- else
- {
- index++;
- }
-
- index++;
- }
-
- var hasSeries = item as IHasSeries;
- if (hasSeriesFields)
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetGuid(index, out var seriesId))
- {
- hasSeries.SeriesId = seriesId;
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.PresentationUniqueKey))
- {
- if (reader.TryGetString(index++, out var presentationUniqueKey))
- {
- item.PresentationUniqueKey = presentationUniqueKey;
- }
- }
-
- if (HasField(query, ItemFields.InheritedParentalRatingValue))
- {
- if (reader.TryGetInt32(index++, out var parentalRating))
- {
- item.InheritedParentalRatingValue = parentalRating;
- }
- }
-
- if (HasField(query, ItemFields.ExternalSeriesId))
- {
- if (reader.TryGetString(index++, out var externalSeriesId))
- {
- item.ExternalSeriesId = externalSeriesId;
- }
- }
-
- if (HasField(query, ItemFields.Taglines))
- {
- if (reader.TryGetString(index++, out var tagLine))
- {
- item.Tagline = tagLine;
- }
- }
-
- if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds))
- {
- DeserializeProviderIds(providerIds, item);
- }
-
- index++;
-
- if (query.DtoOptions.EnableImages)
- {
- if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos))
- {
- item.ImageInfos = DeserializeImages(imageInfos);
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.ProductionLocations))
- {
- if (reader.TryGetString(index++, out var productionLocations))
- {
- item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.ExtraIds))
- {
- if (reader.TryGetString(index++, out var extraIds))
- {
- item.ExtraIds = SplitToGuids(extraIds);
- }
- }
-
- if (reader.TryGetInt32(index++, out var totalBitrate))
- {
- item.TotalBitrate = totalBitrate;
- }
-
- if (reader.TryGetString(index++, out var extraTypeString))
- {
- if (Enum.TryParse(extraTypeString, true, out ExtraType extraType))
- {
- item.ExtraType = extraType;
- }
- }
-
- if (hasArtistFields)
- {
- if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists))
- {
- hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
-
- if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists))
- {
- hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var externalId))
- {
- item.ExternalId = externalId;
- }
-
- if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetString(index, out var seriesPresentationUniqueKey))
- {
- hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- }
- }
-
- index++;
- }
-
- if (enableProgramAttributes)
- {
- if (item is LiveTvProgram program && reader.TryGetString(index, out var showId))
- {
- program.ShowId = showId;
- }
-
- index++;
- }
-
- if (reader.TryGetGuid(index, out var ownerId))
- {
- item.OwnerId = ownerId;
- }
-
- return item;
- }
-
- private static Guid[] SplitToGuids(string value)
- {
- var ids = value.Split('|');
-
- var result = new Guid[ids.Length];
-
- for (var i = 0; i < result.Length; i++)
- {
- result[i] = new Guid(ids[i]);
- }
-
- return result;
- }
-
- ///
- public List GetChapters(BaseItem item)
- {
- CheckDisposed();
-
- var chapters = new List();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
- {
- statement.TryBind("@ItemId", item.Id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- chapters.Add(GetChapter(row, item));
- }
- }
-
- return chapters;
- }
-
- ///
- public ChapterInfo GetChapter(BaseItem item, int index)
- {
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
- {
- statement.TryBind("@ItemId", item.Id);
- statement.TryBind("@ChapterIndex", index);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetChapter(row, item);
- }
- }
-
- return null;
- }
-
- ///
- /// Gets the chapter.
- ///
- /// The reader.
- /// The item.
- /// ChapterInfo.
- private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item)
- {
- var chapter = new ChapterInfo
- {
- StartPositionTicks = reader.GetInt64(0)
- };
-
- if (reader.TryGetString(1, out var chapterName))
- {
- chapter.Name = chapterName;
- }
-
- if (reader.TryGetString(2, out var imagePath))
- {
- chapter.ImagePath = imagePath;
- chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
- }
-
- if (reader.TryReadDateTime(3, out var imageDateModified))
- {
- chapter.ImageDateModified = imageDateModified;
- }
-
- return chapter;
- }
-
- ///
- /// Saves the chapters.
- ///
- /// The item id.
- /// The chapters.
- public void SaveChapters(Guid id, IReadOnlyList chapters)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(chapters);
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // First delete chapters
- using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertChapters(id, chapters, connection);
- transaction.Commit();
- }
-
- private void InsertChapters(Guid idBlob, IReadOnlyList chapters, ManagedConnection db)
- {
- var startIndex = 0;
- var limit = 100;
- var chapterIndex = 0;
-
- const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values ";
- var insertText = new StringBuilder(StartInsertText, 256);
-
- while (startIndex < chapters.Count)
- {
- var endIndex = Math.Min(chapters.Count, startIndex + limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
- }
-
- insertText.Length -= 1; // Remove trailing comma
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", idBlob);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var chapter = chapters[i];
-
- statement.TryBind("@ChapterIndex" + index, chapterIndex);
- statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks);
- statement.TryBind("@Name" + index, chapter.Name);
- statement.TryBind("@ImagePath" + index, chapter.ImagePath);
- statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified);
-
- chapterIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private static bool EnableJoinUserData(InternalItemsQuery query)
- {
- if (query.User is null)
- {
- return false;
- }
-
- var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy));
-
- return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
- || sortingFields.Contains(ItemSortBy.IsPlayed)
- || sortingFields.Contains(ItemSortBy.IsUnplayed)
- || sortingFields.Contains(ItemSortBy.PlayCount)
- || sortingFields.Contains(ItemSortBy.DatePlayed)
- || sortingFields.Contains(ItemSortBy.SeriesDatePlayed)
- || query.IsFavoriteOrLiked.HasValue
- || query.IsFavorite.HasValue
- || query.IsResumable.HasValue
- || query.IsPlayed.HasValue
- || query.IsLiked.HasValue;
- }
-
- private bool HasField(InternalItemsQuery query, ItemFields name)
- {
- switch (name)
- {
- case ItemFields.Tags:
- return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query);
- case ItemFields.CustomRating:
- case ItemFields.ProductionLocations:
- case ItemFields.Settings:
- case ItemFields.OriginalTitle:
- case ItemFields.Taglines:
- case ItemFields.SortName:
- case ItemFields.Studios:
- case ItemFields.ExtraIds:
- case ItemFields.DateCreated:
- case ItemFields.Overview:
- case ItemFields.Genres:
- case ItemFields.DateLastMediaAdded:
- case ItemFields.PresentationUniqueKey:
- case ItemFields.InheritedParentalRatingValue:
- case ItemFields.ExternalSeriesId:
- case ItemFields.SeriesPresentationUniqueKey:
- case ItemFields.DateLastRefreshed:
- case ItemFields.DateLastSaved:
- return query.DtoOptions.ContainsField(name);
- case ItemFields.ServiceName:
- return HasServiceName(query);
- default:
- return true;
- }
- }
-
- private bool HasProgramAttributes(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
- }
-
- private bool HasServiceName(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
- }
-
- private bool HasStartDate(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x));
- }
-
- private bool HasEpisodeAttributes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Episode);
- }
-
- private bool HasTrailerTypes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
- }
-
- private bool HasArtistFields(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
- }
-
- private bool HasSeriesFields(InternalItemsQuery query)
- {
- if (query.ParentType == BaseItemKind.PhotoAlbum)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
- }
-
- private void SetFinalColumnsToSelect(InternalItemsQuery query, List columns)
- {
- foreach (var field in _allItemFields)
- {
- if (!HasField(query, field))
- {
- switch (field)
- {
- case ItemFields.Settings:
- columns.Remove("IsLocked");
- columns.Remove("PreferredMetadataCountryCode");
- columns.Remove("PreferredMetadataLanguage");
- columns.Remove("LockedFields");
- break;
- case ItemFields.ServiceName:
- columns.Remove("ExternalServiceId");
- break;
- case ItemFields.SortName:
- columns.Remove("ForcedSortName");
- break;
- case ItemFields.Taglines:
- columns.Remove("Tagline");
- break;
- case ItemFields.Tags:
- columns.Remove("Tags");
- break;
- case ItemFields.IsHD:
- // do nothing
- break;
- default:
- columns.Remove(field.ToString());
- break;
- }
- }
- }
-
- if (!HasProgramAttributes(query))
- {
- columns.Remove("IsMovie");
- columns.Remove("IsSeries");
- columns.Remove("EpisodeTitle");
- columns.Remove("IsRepeat");
- columns.Remove("ShowId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!HasStartDate(query))
- {
- columns.Remove("StartDate");
- }
-
- if (!HasTrailerTypes(query))
- {
- columns.Remove("TrailerTypes");
- }
-
- if (!HasArtistFields(query))
- {
- columns.Remove("AlbumArtists");
- columns.Remove("Artists");
- }
-
- if (!HasSeriesFields(query))
- {
- columns.Remove("SeriesId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!query.DtoOptions.EnableImages)
- {
- columns.Remove("Images");
- }
-
- if (EnableJoinUserData(query))
- {
- columns.Add("UserDatas.UserId");
- columns.Add("UserDatas.lastPlayedDate");
- columns.Add("UserDatas.playbackPositionTicks");
- columns.Add("UserDatas.playcount");
- columns.Add("UserDatas.isFavorite");
- columns.Add("UserDatas.played");
- columns.Add("UserDatas.rating");
- }
-
- if (query.SimilarTo is not null)
- {
- var item = query.SimilarTo;
-
- var builder = new StringBuilder();
- builder.Append('(');
-
- if (item.InheritedParentalRatingValue == 0)
- {
- builder.Append("((InheritedParentalRatingValue=0) * 10)");
- }
- else
- {
- builder.Append(
- @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
- THEN 0
- ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
- END)");
- }
-
- if (item.ProductionYear.HasValue)
- {
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )");
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
- }
-
- // genres, tags, studios, person, year?
- builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
- builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))");
-
- if (item is MusicArtist)
- {
- // Match albums where the artist is AlbumArtist against other albums.
- // It is assumed that similar albums => similar artists.
- builder.Append(
- @"+ (WITH artistValues AS (
- SELECT DISTINCT albumValues.CleanValue
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
- ), similarArtist AS (
- SELECT albumValues.ItemId
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
- ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
- }
-
- builder.Append(") as SimilarityScore");
-
- columns.Add(builder.ToString());
-
- query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
- query.ExcludeProviderIds = item.ProviderIds;
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- var builder = new StringBuilder();
- builder.Append('(');
-
- builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
- builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)");
-
- if (query.SearchTerm.Length > 1)
- {
- builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
- }
-
- builder.Append(") as SearchScore");
-
- columns.Add(builder.ToString());
- }
- }
-
- private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var searchTerm = query.SearchTerm;
-
- if (string.IsNullOrEmpty(searchTerm))
- {
- return;
- }
-
- searchTerm = FixUnicodeChars(searchTerm);
- searchTerm = GetCleanValue(searchTerm);
-
- var commandText = statement.CommandText;
- if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
- }
-
- if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
- }
- }
-
- private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var item = query.SimilarTo;
-
- if (item is null)
- {
- return;
- }
-
- var commandText = statement.CommandText;
-
- if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemOfficialRating", item.OfficialRating);
- }
-
- if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0);
- }
-
- if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SimilarItemId", item.Id);
- }
-
- if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
- }
- }
-
- private string GetJoinUserDataText(InternalItemsQuery query)
- {
- if (!EnableJoinUserData(query))
- {
- return string.Empty;
- }
-
- return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)";
- }
-
- private string GetGroupBy(InternalItemsQuery query)
- {
- var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
- if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
- }
-
- if (enableGroupByPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey";
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by SeriesPresentationUniqueKey";
- }
-
- return string.Empty;
- }
-
- ///
- public int GetCount(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = new List { "count(distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- var commandText = commandTextBuilder.ToString();
-
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- return statement.SelectScalarInt();
- }
- }
-
- ///
- public List GetItemList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 1024)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var items = new List();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
- if (item is not null)
- {
- items.Add(item);
- }
- }
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.EnableGroupByMetadataKey)
- {
- var limit = query.Limit ?? int.MaxValue;
- limit -= 4;
- var newList = new List();
-
- foreach (var item in items)
- {
- AddItem(newList, item);
-
- if (newList.Count >= limit)
- {
- break;
- }
- }
-
- items = newList;
- }
-
- return items;
- }
-
- private string FixUnicodeChars(string buffer)
- {
- buffer = buffer.Replace('\u2013', '-'); // en dash
- buffer = buffer.Replace('\u2014', '-'); // em dash
- buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- buffer = buffer.Replace('\u2017', '_'); // double low line
- buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
- buffer = buffer.Replace('\u2032', '\''); // prime
- buffer = buffer.Replace('\u2033', '\"'); // double prime
- buffer = buffer.Replace('\u0060', '\''); // grave accent
- return buffer.Replace('\u00B4', '\''); // acute accent
- }
-
- private void AddItem(List items, BaseItem newItem)
- {
- for (var i = 0; i < items.Count; i++)
- {
- var item = items[i];
-
- foreach (var providerId in newItem.ProviderIds)
- {
- if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal))
- {
- continue;
- }
-
- if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal))
- {
- if (newItem.SourceType == SourceType.Library)
- {
- items[i] = newItem;
- }
-
- return;
- }
- }
- }
-
- items.Add(newItem);
- }
-
- ///
- public QueryResult GetItems(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0))
- {
- var returnList = GetItemList(query);
- return new QueryResult(
- query.StartIndex,
- returnList.Count,
- returnList);
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 512)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- string.Join(" AND ", whereClauses);
-
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- var itemQuery = string.Empty;
- var totalRecordCountQuery = string.Empty;
- if (!isReturningZeroItems)
- {
- itemQuery = commandTextBuilder.ToString();
- }
-
- if (query.EnableTotalRecordCount)
- {
- commandTextBuilder.Clear();
-
- commandTextBuilder.Append(" select ");
-
- List columnsToSelect;
- if (EnableGroupByPresentationUniqueKey(query))
- {
- columnsToSelect = new List { "count (distinct PresentationUniqueKey)" };
- }
- else if (query.GroupBySeriesPresentationUniqueKey)
- {
- columnsToSelect = new List { "count (distinct SeriesPresentationUniqueKey)" };
- }
- else
- {
- columnsToSelect = new List { "count (guid)" };
- }
-
- SetFinalColumnsToSelect(query, columnsToSelect);
-
- commandTextBuilder.AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- totalRecordCountQuery = commandTextBuilder.ToString();
- }
-
- var list = new List();
- var result = new QueryResult();
- using var connection = GetConnection(true);
- using var transaction = connection.BeginTransaction();
- if (!isReturningZeroItems)
- {
- using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = PrepareStatement(connection, itemQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- list.Add(item);
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = PrepareStatement(connection, totalRecordCountQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
- return result;
- }
-
- private string GetOrderByText(InternalItemsQuery query)
- {
- var orderBy = query.OrderBy;
- bool hasSimilar = query.SimilarTo is not null;
- bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm);
-
- if (hasSimilar || hasSearch)
- {
- List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
- if (hasSearch)
- {
- prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
- }
-
- if (hasSimilar)
- {
- prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
- }
-
- orderBy = query.OrderBy = [.. prepend, .. orderBy];
- }
- else if (orderBy.Count == 0)
- {
- return string.Empty;
- }
-
- return " ORDER BY " + string.Join(',', orderBy.Select(i =>
- {
- var sortBy = MapOrderByField(i.OrderBy, query);
- var sortOrder = i.SortOrder == SortOrder.Ascending ? "ASC" : "DESC";
- return sortBy + " " + sortOrder;
- }));
- }
-
- private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
- {
- return sortBy switch
- {
- ItemSortBy.AirTime => "SortName", // TODO
- ItemSortBy.Runtime => "RuntimeTicks",
- ItemSortBy.Random => "RANDOM()",
- ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
- ItemSortBy.DatePlayed => "LastPlayedDate",
- ItemSortBy.PlayCount => "PlayCount",
- ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
- ItemSortBy.IsFolder => "IsFolder",
- ItemSortBy.IsPlayed => "played",
- ItemSortBy.IsUnplayed => "played",
- ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
- ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
- ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
- ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
- ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
- ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
- ItemSortBy.SeriesSortName => "SeriesName",
- ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
- ItemSortBy.Album => "Album",
- ItemSortBy.DateCreated => "DateCreated",
- ItemSortBy.PremiereDate => "PremiereDate",
- ItemSortBy.StartDate => "StartDate",
- ItemSortBy.Name => "Name",
- ItemSortBy.CommunityRating => "CommunityRating",
- ItemSortBy.ProductionYear => "ProductionYear",
- ItemSortBy.CriticRating => "CriticRating",
- ItemSortBy.VideoBitRate => "VideoBitRate",
- ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
- ItemSortBy.IndexNumber => "IndexNumber",
- ItemSortBy.SimilarityScore => "SimilarityScore",
- ItemSortBy.SearchScore => "SearchScore",
- _ => "SortName"
- };
- }
-
- ///
- public List GetItemIdsList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var columns = new List { "guid" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var list = new List();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetGuid(0));
- }
- }
-
- return list;
- }
-
- private bool IsAlphaNumeric(string str)
- {
- if (string.IsNullOrWhiteSpace(str))
- {
- return false;
- }
-
- for (int i = 0; i < str.Length; i++)
- {
- if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private bool IsValidPersonType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
-#nullable enable
- private List GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
- {
- if (query.IsResumable ?? false)
- {
- query.IsVirtualItem = false;
- }
-
- var minWidth = query.MinWidth;
- var maxWidth = query.MaxWidth;
-
- if (query.IsHD.HasValue)
- {
- const int Threshold = 1200;
- if (query.IsHD.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- if (query.Is4K.HasValue)
- {
- const int Threshold = 3800;
- if (query.Is4K.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- var whereClauses = new List();
-
- if (minWidth.HasValue)
- {
- whereClauses.Add("Width>=@MinWidth");
- statement?.TryBind("@MinWidth", minWidth);
- }
-
- if (query.MinHeight.HasValue)
- {
- whereClauses.Add("Height>=@MinHeight");
- statement?.TryBind("@MinHeight", query.MinHeight);
- }
-
- if (maxWidth.HasValue)
- {
- whereClauses.Add("Width<=@MaxWidth");
- statement?.TryBind("@MaxWidth", maxWidth);
- }
-
- if (query.MaxHeight.HasValue)
- {
- whereClauses.Add("Height<=@MaxHeight");
- statement?.TryBind("@MaxHeight", query.MaxHeight);
- }
-
- if (query.IsLocked.HasValue)
- {
- whereClauses.Add("IsLocked=@IsLocked");
- statement?.TryBind("@IsLocked", query.IsLocked);
- }
-
- var tags = query.Tags.ToList();
- var excludeTags = query.ExcludeTags.ToList();
-
- if (query.IsMovie == true)
- {
- if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || query.IncludeItemTypes.Contains(BaseItemKind.Trailer))
- {
- whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
- }
- else
- {
- whereClauses.Add("IsMovie=@IsMovie");
- }
-
- statement?.TryBind("@IsMovie", true);
- }
- else if (query.IsMovie.HasValue)
- {
- whereClauses.Add("IsMovie=@IsMovie");
- statement?.TryBind("@IsMovie", query.IsMovie);
- }
-
- if (query.IsSeries.HasValue)
- {
- whereClauses.Add("IsSeries=@IsSeries");
- statement?.TryBind("@IsSeries", query.IsSeries);
- }
-
- if (query.IsSports.HasValue)
- {
- if (query.IsSports.Value)
- {
- tags.Add("Sports");
- }
- else
- {
- excludeTags.Add("Sports");
- }
- }
-
- if (query.IsNews.HasValue)
- {
- if (query.IsNews.Value)
- {
- tags.Add("News");
- }
- else
- {
- excludeTags.Add("News");
- }
- }
-
- if (query.IsKids.HasValue)
- {
- if (query.IsKids.Value)
- {
- tags.Add("Kids");
- }
- else
- {
- excludeTags.Add("Kids");
- }
- }
-
- if (query.SimilarTo is not null && query.MinSimilarityScore > 0)
- {
- whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture));
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- whereClauses.Add("SearchScore > 0");
- }
-
- if (query.IsFolder.HasValue)
- {
- whereClauses.Add("IsFolder=@IsFolder");
- statement?.TryBind("@IsFolder", query.IsFolder);
- }
-
- var includeTypes = query.IncludeItemTypes;
- // Only specify excluded types if no included types are specified
- if (query.IncludeItemTypes.Length == 0)
- {
- var excludeTypes = query.ExcludeItemTypes;
- if (excludeTypes.Length == 1)
- {
- if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
- {
- whereClauses.Add("type<>@type");
- statement?.TryBind("@type", excludeTypeName);
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]);
- }
- }
- else if (excludeTypes.Length > 1)
- {
- var whereBuilder = new StringBuilder("type not in (");
- foreach (var excludeType in excludeTypes)
- {
- if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
- {
- whereBuilder
- .Append('\'')
- .Append(baseItemKindName)
- .Append("',");
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType);
- }
- }
-
- // Remove trailing comma.
- whereBuilder.Length--;
- whereBuilder.Append(')');
- whereClauses.Add(whereBuilder.ToString());
- }
- }
- else if (includeTypes.Length == 1)
- {
- if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
- {
- whereClauses.Add("type=@type");
- statement?.TryBind("@type", includeTypeName);
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]);
- }
- }
- else if (includeTypes.Length > 1)
- {
- var whereBuilder = new StringBuilder("type in (");
- foreach (var includeType in includeTypes)
- {
- if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
- {
- whereBuilder
- .Append('\'')
- .Append(baseItemKindName)
- .Append("',");
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType);
- }
- }
-
- // Remove trailing comma.
- whereBuilder.Length--;
- whereBuilder.Append(')');
- whereClauses.Add(whereBuilder.ToString());
- }
-
- if (query.ChannelIds.Count == 1)
- {
- whereClauses.Add("ChannelId=@ChannelId");
- statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (query.ChannelIds.Count > 1)
- {
- var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add($"ChannelId in ({inClause})");
- }
-
- if (!query.ParentId.IsEmpty())
- {
- whereClauses.Add("ParentId=@ParentId");
- statement?.TryBind("@ParentId", query.ParentId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Path))
- {
- whereClauses.Add("Path=@Path");
- statement?.TryBind("@Path", GetPathToSave(query.Path));
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey");
- statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey);
- }
-
- if (query.MinCommunityRating.HasValue)
- {
- whereClauses.Add("CommunityRating>=@MinCommunityRating");
- statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value);
- }
-
- if (query.MinIndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber>=@MinIndexNumber");
- statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
- }
-
- if (query.MinParentAndIndexNumber.HasValue)
- {
- whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
- statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
- statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
- }
-
- if (query.MinDateCreated.HasValue)
- {
- whereClauses.Add("DateCreated>=@MinDateCreated");
- statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value);
- }
-
- if (query.MinDateLastSaved.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value);
- }
-
- if (query.MinDateLastSavedForUser.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value);
- }
-
- if (query.IndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber=@IndexNumber");
- statement?.TryBind("@IndexNumber", query.IndexNumber.Value);
- }
-
- if (query.ParentIndexNumber.HasValue)
- {
- whereClauses.Add("ParentIndexNumber=@ParentIndexNumber");
- statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
- }
-
- if (query.ParentIndexNumberNotEquals.HasValue)
- {
- whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)");
- statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value);
- }
-
- var minEndDate = query.MinEndDate;
- var maxEndDate = query.MaxEndDate;
-
- if (query.HasAired.HasValue)
- {
- if (query.HasAired.Value)
- {
- maxEndDate = DateTime.UtcNow;
- }
- else
- {
- minEndDate = DateTime.UtcNow;
- }
- }
-
- if (minEndDate.HasValue)
- {
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", minEndDate.Value);
- }
-
- if (maxEndDate.HasValue)
- {
- whereClauses.Add("EndDate<=@MaxEndDate");
- statement?.TryBind("@MaxEndDate", maxEndDate.Value);
- }
-
- if (query.MinStartDate.HasValue)
- {
- whereClauses.Add("StartDate>=@MinStartDate");
- statement?.TryBind("@MinStartDate", query.MinStartDate.Value);
- }
-
- if (query.MaxStartDate.HasValue)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value);
- }
-
- if (query.MinPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate>=@MinPremiereDate");
- statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value);
- }
-
- if (query.MaxPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate<=@MaxPremiereDate");
- statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
- }
-
- StringBuilder clauseBuilder = new StringBuilder();
- const string Or = " OR ";
-
- var trailerTypes = query.TrailerTypes;
- int trailerTypesLen = trailerTypes.Length;
- if (trailerTypesLen > 0)
- {
- clauseBuilder.Append('(');
-
- for (int i = 0; i < trailerTypesLen; i++)
- {
- var paramName = "@TrailerTypes" + i;
- clauseBuilder.Append("TrailerTypes like ")
- .Append(paramName)
- .Append(Or);
- statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (query.IsAiring.HasValue)
- {
- if (query.IsAiring.Value)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", DateTime.UtcNow);
-
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", DateTime.UtcNow);
- }
- else
- {
- whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)");
- statement?.TryBind("@IsAiringDate", DateTime.UtcNow);
- }
- }
-
- int personIdsLen = query.PersonIds.Length;
- if (personIdsLen > 0)
- {
- // TODO: Should this query with CleanName ?
-
- clauseBuilder.Append('(');
-
- Span idBytes = stackalloc byte[16];
- for (int i = 0; i < personIdsLen; i++)
- {
- string paramName = "@PersonId" + i;
- clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
- .Append(paramName)
- .Append("))) OR ");
-
- statement?.TryBind(paramName, query.PersonIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (!string.IsNullOrWhiteSpace(query.Person))
- {
- whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)");
- statement?.TryBind("@PersonName", query.Person);
- }
-
- if (!string.IsNullOrWhiteSpace(query.MinSortName))
- {
- whereClauses.Add("SortName>=@MinSortName");
- statement?.TryBind("@MinSortName", query.MinSortName);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId))
- {
- whereClauses.Add("ExternalSeriesId=@ExternalSeriesId");
- statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalId))
- {
- whereClauses.Add("ExternalId=@ExternalId");
- statement?.TryBind("@ExternalId", query.ExternalId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Name))
- {
- whereClauses.Add("CleanName=@Name");
- statement?.TryBind("@Name", GetCleanValue(query.Name));
- }
-
- // These are the same, for now
- var nameContains = query.NameContains;
- if (!string.IsNullOrWhiteSpace(nameContains))
- {
- whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)");
- if (statement is not null)
- {
- nameContains = FixUnicodeChars(nameContains);
- statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
- {
- whereClauses.Add("SortName like @NameStartsWith");
- statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%");
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
- {
- whereClauses.Add("SortName >= @NameStartsWithOrGreater");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant());
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameLessThan))
- {
- whereClauses.Add("SortName < @NameLessThan");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant());
- }
-
- if (query.ImageTypes.Length > 0)
- {
- foreach (var requiredImage in query.ImageTypes)
- {
- whereClauses.Add("Images like '%" + requiredImage + "%'");
- }
- }
-
- if (query.IsLiked.HasValue)
- {
- if (query.IsLiked.Value)
- {
- whereClauses.Add("rating>=@UserRating");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- else
- {
- whereClauses.Add("(rating is null or rating<@UserRating)");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- }
-
- if (query.IsFavoriteOrLiked.HasValue)
- {
- if (query.IsFavoriteOrLiked.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavoriteOrLiked");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)");
- }
-
- statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value);
- }
-
- if (query.IsFavorite.HasValue)
- {
- if (query.IsFavorite.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavorite");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)");
- }
-
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- }
-
- if (EnableJoinUserData(query))
- {
- if (query.IsPlayed.HasValue)
- {
- // We should probably figure this out for all folders, but for right now, this is the only place where we need it
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series)
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- else
- {
- whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- }
- else
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("(played=@IsPlayed)");
- }
- else
- {
- whereClauses.Add("(played is null or played=@IsPlayed)");
- }
-
- statement?.TryBind("@IsPlayed", query.IsPlayed.Value);
- }
- }
- }
-
- if (query.IsResumable.HasValue)
- {
- if (query.IsResumable.Value)
- {
- whereClauses.Add("playbackPositionTicks > 0");
- }
- else
- {
- whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)");
- }
- }
-
- if (query.ArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ContributingArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ContributingArtistIds.Length; i++)
- {
- clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumIds.Length; i++)
- {
- clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds")
- .Append(i)
- .Append(") OR ");
- statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ExcludeArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ExcludeArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.GenreIds.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.GenreIds.Count; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId")
- .Append(i)
- .Append(") and Type=2)) OR ");
- statement?.TryBind("@GenreId" + i, query.GenreIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.Genres.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.Genres.Count; i++)
- {
- clauseBuilder.Append("@Genre")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR ");
- statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (tags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < tags.Count; i++)
- {
- clauseBuilder.Append("@Tag")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@Tag" + i, GetCleanValue(tags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (excludeTags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < excludeTags.Count; i++)
- {
- clauseBuilder.Append("@ExcludeTag")
- .Append(i)
- .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.StudioIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.StudioIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId")
- .Append(i)
- .Append(") and Type=3)) OR ");
- statement?.TryBind("@StudioId" + i, query.StudioIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.OfficialRatings.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.OfficialRatings.Length; i++)
- {
- clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or);
- statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- clauseBuilder.Append('(');
- if (query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue not null");
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- }
- else if (query.BlockUnratedItems.Length > 0)
- {
- const string ParamName = "@UnratedType";
- clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (");
-
- for (int i = 0; i < query.BlockUnratedItems.Length; i++)
- {
- clauseBuilder.Append(ParamName).Append(i).Append(',');
- statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString());
- }
-
- // Remove trailing comma
- clauseBuilder.Length--;
- clauseBuilder.Append("))");
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" OR (");
- }
-
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND ");
- }
-
- clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(')');
- }
-
- if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
- {
- clauseBuilder.Append(" OR InheritedParentalRatingValue not null");
- }
- }
- else if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- clauseBuilder.Append(')');
- }
- else if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- else if (!query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null");
- }
-
- if (clauseBuilder.Length > 1)
- {
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.HasOfficialRating.HasValue)
- {
- if (query.HasOfficialRating.Value)
- {
- whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')");
- }
- else
- {
- whereClauses.Add("(OfficialRating is null OR OfficialRating='')");
- }
- }
-
- if (query.HasOverview.HasValue)
- {
- if (query.HasOverview.Value)
- {
- whereClauses.Add("(Overview not null AND Overview<>'')");
- }
- else
- {
- whereClauses.Add("(Overview is null OR Overview='')");
- }
- }
-
- if (query.HasOwnerId.HasValue)
- {
- if (query.HasOwnerId.Value)
- {
- whereClauses.Add("OwnerId not null");
- }
- else
- {
- whereClauses.Add("OwnerId is null");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
- }
-
- if (query.HasSubtitles.HasValue)
- {
- if (query.HasSubtitles.Value)
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)");
- }
- }
-
- if (query.HasChapterImages.HasValue)
- {
- if (query.HasChapterImages.Value)
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)");
- }
- }
-
- if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value)
- {
- whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)");
- }
-
- if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))");
- }
-
- if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)");
- }
-
- if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value)
- {
- whereClauses.Add("Name not in (Select Name From People)");
- }
-
- if (query.Years.Length == 1)
- {
- whereClauses.Add("ProductionYear=@Years");
- statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
- }
- else if (query.Years.Length > 1)
- {
- var val = string.Join(',', query.Years);
- whereClauses.Add("ProductionYear in (" + val + ")");
- }
-
- var isVirtualItem = query.IsVirtualItem ?? query.IsMissing;
- if (isVirtualItem.HasValue)
- {
- whereClauses.Add("IsVirtualItem=@IsVirtualItem");
- statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
- }
-
- if (query.IsSpecialSeason.HasValue)
- {
- if (query.IsSpecialSeason.Value)
- {
- whereClauses.Add("IndexNumber = 0");
- }
- else
- {
- whereClauses.Add("IndexNumber <> 0");
- }
- }
-
- if (query.IsUnaired.HasValue)
- {
- if (query.IsUnaired.Value)
- {
- whereClauses.Add("PremiereDate >= DATETIME('now')");
- }
- else
- {
- whereClauses.Add("PremiereDate < DATETIME('now')");
- }
- }
-
- if (query.MediaTypes.Length == 1)
- {
- whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
- }
- else if (query.MediaTypes.Length > 1)
- {
- var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
- whereClauses.Add("MediaType in (" + val + ")");
- }
-
- if (query.ItemIds.Length > 0)
- {
- var includeIds = new List();
- var index = 0;
- foreach (var id in query.ItemIds)
- {
- includeIds.Add("Guid = @IncludeId" + index);
- statement?.TryBind("@IncludeId" + index, id);
- index++;
- }
-
- whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")");
- }
-
- if (query.ExcludeItemIds.Length > 0)
- {
- var excludeIds = new List();
- var index = 0;
- foreach (var id in query.ExcludeItemIds)
- {
- excludeIds.Add("Guid <> @ExcludeId" + index);
- statement?.TryBind("@ExcludeId" + index, id);
- index++;
- }
-
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
-
- if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0)
- {
- var excludeIds = new List();
-
- var index = 0;
- foreach (var pair in query.ExcludeProviderIds)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var paramName = "@ExcludeProviderId" + index;
- excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (excludeIds.Count > 0)
- {
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
- }
-
- if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0)
- {
- var hasProviderIds = new List();
-
- var index = 0;
- foreach (var pair in query.HasAnyProviderId)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // TODO this seems to be an idea for a better schema where ProviderIds are their own table
- // but this is not implemented
- // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
-
- // TODO this is a really BAD way to do it since the pair:
- // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567
- // and maybe even NotTmdb=1234.
-
- // this is a placeholder for this specific pair to correlate it in the bigger query
- var paramName = "@HasAnyProviderId" + index;
-
- // this is a search for the placeholder
- hasProviderIds.Add("ProviderIds like " + paramName);
-
- // this replaces the placeholder with a value, here: %key=val%
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (hasProviderIds.Count > 0)
- {
- whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")");
- }
- }
-
- if (query.HasImdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb"));
- }
-
- if (query.HasTmdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb"));
- }
-
- if (query.HasTvdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
- }
-
- var queryTopParentIds = query.TopParentIds;
-
- if (queryTopParentIds.Length > 0)
- {
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
- if (queryTopParentIds.Length == 1)
- {
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
-
- statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
- }
- }
- }
-
- if (query.AncestorIds.Length == 1)
- {
- whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
- statement?.TryBind("@AncestorId", query.AncestorIds[0]);
- }
-
- if (query.AncestorIds.Length > 1)
- {
- var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
- }
-
- if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
- {
- var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
- statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
- }
-
- if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
- {
- whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
- statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
- }
-
- if (query.ExcludeInheritedTags.Length > 0)
- {
- var paramName = "@ExcludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
- }
- else
- {
- for (int index = 0; index < query.ExcludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index]));
- }
- }
- }
-
- if (query.IncludeInheritedTags.Length > 0)
- {
- var paramName = "@IncludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
- // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
- // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
- if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
- """);
- }
-
- // A playlist should be accessible to its owner regardless of allowed tags.
- else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR data like @PlaylistOwnerUserId)
- """);
- }
- else
- {
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
- }
- }
- else
- {
- for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
- }
-
- if (query.User is not null)
- {
- statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%""");
- }
- }
- }
-
- if (query.SeriesStatuses.Length > 0)
- {
- var statuses = new List();
-
- foreach (var seriesStatus in query.SeriesStatuses)
- {
- statuses.Add("data like '%" + seriesStatus + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", statuses) + ")");
- }
-
- if (query.BoxSetLibraryFolders.Length > 0)
- {
- var folderIdQueries = new List();
-
- foreach (var folderId in query.BoxSetLibraryFolders)
- {
- folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")");
- }
-
- if (query.VideoTypes.Length > 0)
- {
- var videoTypes = new List();
-
- foreach (var videoType in query.VideoTypes)
- {
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
- }
-
- if (query.Is3D.HasValue)
- {
- if (query.Is3D.Value)
- {
- whereClauses.Add("data like '%Video3DFormat%'");
- }
- else
- {
- whereClauses.Add("data not like '%Video3DFormat%'");
- }
- }
-
- if (query.IsPlaceHolder.HasValue)
- {
- if (query.IsPlaceHolder.Value)
- {
- whereClauses.Add("data like '%\"IsPlaceHolder\":true%'");
- }
- else
- {
- whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')");
- }
- }
-
- if (query.HasSpecialFeature.HasValue)
- {
- if (query.HasSpecialFeature.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasTrailer.HasValue)
- {
- if (query.HasTrailer.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeSong.HasValue)
- {
- if (query.HasThemeSong.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeVideo.HasValue)
- {
- if (query.HasThemeVideo.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- return whereClauses;
- }
-
- ///
- /// Formats a where clause for the specified provider.
- ///
- /// Whether or not to include items with this provider's ids.
- /// Provider name.
- /// Formatted SQL clause.
- private string GetProviderIdClause(bool includeResults, string provider)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- "ProviderIds {0} like '%{1}=%'",
- includeResults ? string.Empty : "not",
- provider);
- }
-
-#nullable disable
- private List GetItemByNameTypesInQuery(InternalItemsQuery query)
- {
- var list = new List();
-
- if (IsTypeInQuery(BaseItemKind.Person, query))
- {
- list.Add(typeof(Person).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Genre, query))
- {
- list.Add(typeof(Genre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
- {
- list.Add(typeof(MusicGenre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
- {
- list.Add(typeof(MusicArtist).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Studio, query))
- {
- list.Add(typeof(Studio).FullName);
- }
-
- return list;
- }
-
- private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
- {
- if (query.ExcludeItemTypes.Contains(type))
- {
- return false;
- }
-
- return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
- }
-
- private string GetCleanValue(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return value;
- }
-
- return value.RemoveDiacritics().ToLowerInvariant();
- }
-
- private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
- {
- if (!query.GroupByPresentationUniqueKey)
- {
- return false;
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return false;
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- return false;
- }
-
- if (query.User is null)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
- || query.IncludeItemTypes.Contains(BaseItemKind.Video)
- || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
- || query.IncludeItemTypes.Contains(BaseItemKind.Series)
- || query.IncludeItemTypes.Contains(BaseItemKind.Season);
- }
-
- ///
- public void UpdateInheritedValues()
- {
- const string Statements = """
-delete from ItemValues where type = 6;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
-FROM AncestorIds
-LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
-where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4;
-""";
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- connection.Execute(Statements);
- transaction.Commit();
- }
-
- ///
- public void DeleteItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete people
- ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id);
-
- // Delete chapters
- ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id);
-
- // Delete media streams
- ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id);
-
- // Delete ancestors
- ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id);
-
- // Delete item values
- ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id);
-
- // Delete the item
- ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id);
-
- transaction.Commit();
- }
-
- private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
- {
- using (var statement = PrepareStatement(db, query))
- {
- statement.TryBind("@Id", value);
-
- statement.ExecuteNonQuery();
- }
- }
-
- ///
- public List GetPeopleNames(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var commandText = new StringBuilder("select Distinct p.Name from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetString(0));
- }
- }
-
- return list;
- }
-
- ///
- public List GetPeople(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetPerson(row));
- }
- }
-
- return list;
- }
-
- private List GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
- {
- var whereClauses = new List();
-
- if (query.User is not null && query.IsFavorite.HasValue)
- {
- whereClauses.Add(@"p.Name IN (
-SELECT Name FROM TypedBaseItems WHERE UserDataKey IN (
-SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
-AND Type = @InternalPersonType)");
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
- statement?.TryBind("@UserId", query.User.InternalId);
- }
-
- if (!query.ItemId.IsEmpty())
- {
- whereClauses.Add("ItemId=@ItemId");
- statement?.TryBind("@ItemId", query.ItemId);
- }
-
- if (!query.AppearsInItemId.IsEmpty())
- {
- whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
- statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
- }
-
- var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryPersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType=@PersonType");
- statement?.TryBind("@PersonType", queryPersonTypes[0]);
- }
- else if (queryPersonTypes.Count > 1)
- {
- var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType in (" + val + ")");
- }
-
- var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryExcludePersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType<>@PersonType");
- statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
- }
- else if (queryExcludePersonTypes.Count > 1)
- {
- var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType not in (" + val + ")");
- }
-
- if (query.MaxListOrder.HasValue)
- {
- whereClauses.Add("ListOrder<=@MaxListOrder");
- statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameContains))
- {
- whereClauses.Add("p.Name like @NameContains");
- statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
- }
-
- return whereClauses;
- }
-
- private void UpdateAncestors(Guid itemId, List ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(ancestorIds);
-
- CheckDisposed();
-
- // First delete
- deleteAncestorsStatement.TryBind("@ItemId", itemId);
- deleteAncestorsStatement.ExecuteNonQuery();
-
- if (ancestorIds.Count == 0)
- {
- return;
- }
-
- var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values ");
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", itemId);
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var ancestorId = ancestorIds[i];
-
- statement.TryBind("@AncestorId" + index, ancestorId);
- statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
- }
-
- ///
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
- }
-
- ///
- public List GetStudioNames()
- {
- return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty());
- }
-
- ///
- public List GetAllArtistNames()
- {
- return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty());
- }
-
- ///
- public List GetMusicGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- },
- Array.Empty());
- }
-
- ///
- public List GetGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- Array.Empty(),
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- });
- }
-
- private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes)
- {
- CheckDisposed();
-
- var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
- if (itemValueTypes.Length == 1)
- {
- stringBuilder.Append('=')
- .Append(itemValueTypes[0]);
- }
- else
- {
- stringBuilder.Append(" in (")
- .AppendJoin(',', itemValueTypes)
- .Append(')');
- }
-
- if (withItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', withItemTypes)
- .Append("))");
- }
-
- if (excludeItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', excludeItemTypes)
- .Append("))");
- }
-
- stringBuilder.Append(" Group By CleanValue");
- var commandText = stringBuilder.ToString();
-
- var list = new List();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- if (row.TryGetString(0, out var result))
- {
- list.Add(result);
- }
- }
- }
-
- return list;
- }
-
- private QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- if (!query.Limit.HasValue)
- {
- query.EnableTotalRecordCount = false;
- }
-
- CheckDisposed();
-
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0]) :
- ("Type in (" + string.Join(',', itemValueTypes) + ")");
-
- InternalItemsQuery typeSubQuery = null;
-
- string itemCountColumns = null;
-
- var stringBuilder = new StringBuilder(1024);
- var typesToCount = query.IncludeItemTypes;
-
- if (typesToCount.Length > 0)
- {
- stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
-
- typeSubQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ExcludeItemIds = query.ExcludeItemIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsPlayed = query.IsPlayed
- };
- var whereClauses = GetWhereClauses(typeSubQuery, null);
-
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses)
- .Append(" AND ")
- .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
- .Append(typeClause)
- .Append(")) as itemTypes");
-
- itemCountColumns = stringBuilder.ToString();
- stringBuilder.Clear();
- }
-
- List columns = _retrieveItemColumns.ToList();
- // Unfortunately we need to add it to columns to ensure the order of the columns in the select
- if (!string.IsNullOrEmpty(itemCountColumns))
- {
- columns.Add(itemCountColumns);
- }
-
- // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
- var innerQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsAiring = query.IsAiring,
- IsMovie = query.IsMovie,
- IsSports = query.IsSports,
- IsKids = query.IsKids,
- IsNews = query.IsNews,
- IsSeries = query.IsSeries
- };
-
- SetFinalColumnsToSelect(query, columns);
-
- var innerWhereClauses = GetWhereClauses(innerQuery, null);
-
- stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
- .Append(typeClause)
- .Append(" AND ItemId in (select guid from TypedBaseItems");
- if (innerWhereClauses.Count > 0)
- {
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", innerWhereClauses);
- }
-
- stringBuilder.Append("))");
-
- var outerQuery = new InternalItemsQuery(query.User)
- {
- IsPlayed = query.IsPlayed,
- IsFavorite = query.IsFavorite,
- IsFavoriteOrLiked = query.IsFavoriteOrLiked,
- IsLiked = query.IsLiked,
- IsLocked = query.IsLocked,
- NameLessThan = query.NameLessThan,
- NameStartsWith = query.NameStartsWith,
- NameStartsWithOrGreater = query.NameStartsWithOrGreater,
- Tags = query.Tags,
- OfficialRatings = query.OfficialRatings,
- StudioIds = query.StudioIds,
- GenreIds = query.GenreIds,
- Genres = query.Genres,
- Years = query.Years,
- NameContains = query.NameContains,
- SearchTerm = query.SearchTerm,
- SimilarTo = query.SimilarTo,
- ExcludeItemIds = query.ExcludeItemIds
- };
-
- var outerWhereClauses = GetWhereClauses(outerQuery, null);
- if (outerWhereClauses.Count != 0)
- {
- stringBuilder.Append(" AND ")
- .AppendJoin(" AND ", outerWhereClauses);
- }
-
- var whereText = stringBuilder.ToString();
- stringBuilder.Clear();
-
- stringBuilder.Append("select ")
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText)
- .Append(" group by PresentationUniqueKey");
-
- if (query.OrderBy.Count != 0
- || query.SimilarTo is not null
- || !string.IsNullOrEmpty(query.SearchTerm))
- {
- stringBuilder.Append(GetOrderByText(query));
- }
- else
- {
- stringBuilder.Append(" order by SortName");
- }
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- stringBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- stringBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- string commandText = string.Empty;
-
- if (!isReturningZeroItems)
- {
- commandText = stringBuilder.ToString();
- }
-
- string countText = string.Empty;
- if (query.EnableTotalRecordCount)
- {
- stringBuilder.Clear();
- var columnsToSelect = new List { "count (distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columnsToSelect);
- stringBuilder.Append("select ")
- .AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText);
-
- countText = stringBuilder.ToString();
- }
-
- var list = new List<(BaseItem, ItemCounts)>();
- var result = new QueryResult<(BaseItem, ItemCounts)>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var transaction = connection.BeginTransaction())
- {
- if (!isReturningZeroItems)
- {
- using (var statement = PrepareStatement(connection, commandText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- var countStartColumn = columns.Count - 1;
-
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (var statement = PrepareStatement(connection, countText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
- }
-
- if (result.TotalRecordCount == 0)
- {
- result.TotalRecordCount = list.Count;
- }
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
-
- return result;
- }
-
- private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount)
- {
- var counts = new ItemCounts();
-
- if (typesToCount.Length == 0)
- {
- return counts;
- }
-
- if (!reader.TryGetString(countStartColumn, out var typeString))
- {
- return counts;
- }
-
- foreach (var typeName in typeString.AsSpan().Split('|'))
- {
- if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SeriesCount++;
- }
- else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.EpisodeCount++;
- }
- else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.MovieCount++;
- }
- else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.AlbumCount++;
- }
- else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.ArtistCount++;
- }
- else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SongCount++;
- }
- else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.TrailerCount++;
- }
-
- counts.ItemCount++;
- }
-
- return counts;
- }
-
- private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags)
- {
- var list = new List<(int, string)>();
-
- if (item is IHasArtist hasArtist)
- {
- list.AddRange(hasArtist.Artists.Select(i => (0, i)));
- }
-
- if (item is IHasAlbumArtist hasAlbumArtist)
- {
- list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
- }
-
- list.AddRange(item.Genres.Select(i => (2, i)));
- list.AddRange(item.Studios.Select(i => (3, i)));
- list.AddRange(item.Tags.Select(i => (4, i)));
-
- // keywords was 5
-
- list.AddRange(inheritedTags.Select(i => (6, i)));
-
- // Remove all invalid values.
- list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
-
- return list;
- }
-
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(values);
-
- CheckDisposed();
-
- // First delete
- using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id");
- command.TryBind("@Id", itemId);
- command.ExecuteNonQuery();
-
- InsertItemValues(itemId, values, db);
- }
-
- private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
-
- const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < values.Count)
- {
- var endIndex = Math.Min(values.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),",
- i);
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var currentValueInfo = values[i];
-
- var itemValue = currentValueInfo.Value;
-
- statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
- statement.TryBind("@Value" + index, itemValue);
- statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- ///
- public void UpdatePeople(Guid itemId, List people)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete all existing people first
- using var command = connection.CreateCommand();
- command.CommandText = "delete from People where ItemId=@ItemId";
- command.TryBind("@ItemId", itemId);
- command.ExecuteNonQuery();
-
- if (people is not null)
- {
- InsertPeople(itemId, people, connection);
- }
-
- transaction.Commit();
- }
-
- private void InsertPeople(Guid id, List people, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
- var listIndex = 0;
-
- const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < people.Count)
- {
- var endIndex = Math.Min(people.Count, startIndex + Limit);
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var person = people[i];
-
- statement.TryBind("@Name" + index, person.Name);
- statement.TryBind("@Role" + index, person.Role);
- statement.TryBind("@PersonType" + index, person.Type.ToString());
- statement.TryBind("@SortOrder" + index, person.SortOrder);
- statement.TryBind("@ListOrder" + index, listIndex);
-
- listIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private PersonInfo GetPerson(SqliteDataReader reader)
- {
- var item = new PersonInfo
- {
- ItemId = reader.GetGuid(0),
- Name = reader.GetString(1)
- };
-
- if (reader.TryGetString(2, out var role))
- {
- item.Role = role;
- }
-
- if (reader.TryGetString(3, out var type)
- && Enum.TryParse(type, true, out PersonKind personKind))
- {
- item.Type = personKind;
- }
-
- if (reader.TryGetInt32(4, out var sortOrder))
- {
- item.SortOrder = sortOrder;
- }
-
- return item;
- }
-
- ///
- public List GetMediaStreams(MediaStreamQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaStreamSaveColumnsSelectQuery;
-
- if (query.Type.HasValue)
- {
- cmdText += " AND StreamType=@StreamType";
- }
-
- if (query.Index.HasValue)
- {
- cmdText += " AND StreamIndex=@StreamIndex";
- }
-
- cmdText += " order by StreamIndex ASC";
-
- using (var connection = GetConnection(true))
- {
- var list = new List();
-
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Type.HasValue)
- {
- statement.TryBind("@StreamType", query.Type.Value.ToString());
- }
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@StreamIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaStream(row));
- }
- }
-
- return list;
- }
- }
-
- ///
- public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(streams);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete existing mediastreams
- using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaStreams(id, streams, connection);
-
- transaction.Commit();
- }
-
- private void InsertMediaStreams(Guid id, IReadOnlyList streams, ManagedConnection db)
- {
- const int Limit = 10;
- var startIndex = 0;
-
- var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
- while (startIndex < streams.Count)
- {
- var endIndex = Math.Min(streams.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- if (i != startIndex)
- {
- insertText.Append(',');
- }
-
- var index = i.ToString(CultureInfo.InvariantCulture);
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaStreamSaveColumns.Skip(1))
- {
- insertText.Append('@').Append(column).Append(index).Append(',');
- }
-
- insertText.Length -= 1; // Remove the last comma
-
- insertText.Append(')');
- }
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var stream = streams[i];
-
- statement.TryBind("@StreamIndex" + index, stream.Index);
- statement.TryBind("@StreamType" + index, stream.Type.ToString());
- statement.TryBind("@Codec" + index, stream.Codec);
- statement.TryBind("@Language" + index, stream.Language);
- statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout);
- statement.TryBind("@Profile" + index, stream.Profile);
- statement.TryBind("@AspectRatio" + index, stream.AspectRatio);
- statement.TryBind("@Path" + index, GetPathToSave(stream.Path));
-
- statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced);
- statement.TryBind("@BitRate" + index, stream.BitRate);
- statement.TryBind("@Channels" + index, stream.Channels);
- statement.TryBind("@SampleRate" + index, stream.SampleRate);
-
- statement.TryBind("@IsDefault" + index, stream.IsDefault);
- statement.TryBind("@IsForced" + index, stream.IsForced);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
-
- // Yes these are backwards due to a mistake
- statement.TryBind("@Width" + index, stream.Height);
- statement.TryBind("@Height" + index, stream.Width);
-
- statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate);
- statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate);
- statement.TryBind("@Level" + index, stream.Level);
-
- statement.TryBind("@PixelFormat" + index, stream.PixelFormat);
- statement.TryBind("@BitDepth" + index, stream.BitDepth);
- statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
- statement.TryBind("@RefFrames" + index, stream.RefFrames);
-
- statement.TryBind("@CodecTag" + index, stream.CodecTag);
- statement.TryBind("@Comment" + index, stream.Comment);
- statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize);
- statement.TryBind("@IsAvc" + index, stream.IsAVC);
- statement.TryBind("@Title" + index, stream.Title);
-
- statement.TryBind("@TimeBase" + index, stream.TimeBase);
- statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase);
-
- statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
- statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
- statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
-
- statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
- statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
- statement.TryBind("@DvProfile" + index, stream.DvProfile);
- statement.TryBind("@DvLevel" + index, stream.DvLevel);
- statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
- statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
- statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
- statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
-
- statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
-
- statement.TryBind("@Rotation" + index, stream.Rotation);
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
- }
- }
-
- ///
- /// Gets the media stream.
- ///
- /// The reader.
- /// MediaStream.
- private MediaStream GetMediaStream(SqliteDataReader reader)
- {
- var item = new MediaStream
- {
- Index = reader.GetInt32(1),
- Type = Enum.Parse(reader.GetString(2), true)
- };
-
- if (reader.TryGetString(3, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(4, out var language))
- {
- item.Language = language;
- }
-
- if (reader.TryGetString(5, out var channelLayout))
- {
- item.ChannelLayout = channelLayout;
- }
-
- if (reader.TryGetString(6, out var profile))
- {
- item.Profile = profile;
- }
-
- if (reader.TryGetString(7, out var aspectRatio))
- {
- item.AspectRatio = aspectRatio;
- }
-
- if (reader.TryGetString(8, out var path))
- {
- item.Path = RestorePath(path);
- }
-
- item.IsInterlaced = reader.GetBoolean(9);
-
- if (reader.TryGetInt32(10, out var bitrate))
- {
- item.BitRate = bitrate;
- }
-
- if (reader.TryGetInt32(11, out var channels))
- {
- item.Channels = channels;
- }
-
- if (reader.TryGetInt32(12, out var sampleRate))
- {
- item.SampleRate = sampleRate;
- }
-
- item.IsDefault = reader.GetBoolean(13);
- item.IsForced = reader.GetBoolean(14);
- item.IsExternal = reader.GetBoolean(15);
-
- if (reader.TryGetInt32(16, out var width))
- {
- item.Width = width;
- }
-
- if (reader.TryGetInt32(17, out var height))
- {
- item.Height = height;
- }
-
- if (reader.TryGetSingle(18, out var averageFrameRate))
- {
- item.AverageFrameRate = averageFrameRate;
- }
-
- if (reader.TryGetSingle(19, out var realFrameRate))
- {
- item.RealFrameRate = realFrameRate;
- }
-
- if (reader.TryGetSingle(20, out var level))
- {
- item.Level = level;
- }
-
- if (reader.TryGetString(21, out var pixelFormat))
- {
- item.PixelFormat = pixelFormat;
- }
-
- if (reader.TryGetInt32(22, out var bitDepth))
- {
- item.BitDepth = bitDepth;
- }
-
- if (reader.TryGetBoolean(23, out var isAnamorphic))
- {
- item.IsAnamorphic = isAnamorphic;
- }
-
- if (reader.TryGetInt32(24, out var refFrames))
- {
- item.RefFrames = refFrames;
- }
-
- if (reader.TryGetString(25, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(26, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(27, out var nalLengthSize))
- {
- item.NalLengthSize = nalLengthSize;
- }
-
- if (reader.TryGetBoolean(28, out var isAVC))
- {
- item.IsAVC = isAVC;
- }
-
- if (reader.TryGetString(29, out var title))
- {
- item.Title = title;
- }
-
- if (reader.TryGetString(30, out var timeBase))
- {
- item.TimeBase = timeBase;
- }
-
- if (reader.TryGetString(31, out var codecTimeBase))
- {
- item.CodecTimeBase = codecTimeBase;
- }
-
- if (reader.TryGetString(32, out var colorPrimaries))
- {
- item.ColorPrimaries = colorPrimaries;
- }
-
- if (reader.TryGetString(33, out var colorSpace))
- {
- item.ColorSpace = colorSpace;
- }
-
- if (reader.TryGetString(34, out var colorTransfer))
- {
- item.ColorTransfer = colorTransfer;
- }
-
- if (reader.TryGetInt32(35, out var dvVersionMajor))
- {
- item.DvVersionMajor = dvVersionMajor;
- }
-
- if (reader.TryGetInt32(36, out var dvVersionMinor))
- {
- item.DvVersionMinor = dvVersionMinor;
- }
-
- if (reader.TryGetInt32(37, out var dvProfile))
- {
- item.DvProfile = dvProfile;
- }
-
- if (reader.TryGetInt32(38, out var dvLevel))
- {
- item.DvLevel = dvLevel;
- }
-
- if (reader.TryGetInt32(39, out var rpuPresentFlag))
- {
- item.RpuPresentFlag = rpuPresentFlag;
- }
-
- if (reader.TryGetInt32(40, out var elPresentFlag))
- {
- item.ElPresentFlag = elPresentFlag;
- }
-
- if (reader.TryGetInt32(41, out var blPresentFlag))
- {
- item.BlPresentFlag = blPresentFlag;
- }
-
- if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
- {
- item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
- }
-
- item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
-
- if (reader.TryGetInt32(44, out var rotation))
- {
- item.Rotation = rotation;
- }
-
- if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
- {
- item.LocalizedDefault = _localization.GetLocalizedString("Default");
- item.LocalizedExternal = _localization.GetLocalizedString("External");
-
- if (item.Type is MediaStreamType.Subtitle)
- {
- item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
- item.LocalizedForced = _localization.GetLocalizedString("Forced");
- item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
- }
- }
-
- return item;
- }
-
- ///
- public List GetMediaAttachments(MediaAttachmentQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
-
- if (query.Index.HasValue)
- {
- cmdText += " AND AttachmentIndex=@AttachmentIndex";
- }
-
- cmdText += " order by AttachmentIndex ASC";
-
- var list = new List();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@AttachmentIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaAttachment(row));
- }
- }
-
- return list;
- }
-
- ///
- public void SaveMediaAttachments(
- Guid id,
- IReadOnlyList attachments,
- CancellationToken cancellationToken)
- {
- CheckDisposed();
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty.", nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(attachments);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"))
- {
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaAttachments(id, attachments, connection, cancellationToken);
-
- transaction.Commit();
- }
- }
-
- private void InsertMediaAttachments(
- Guid id,
- IReadOnlyList attachments,
- ManagedConnection db,
- CancellationToken cancellationToken)
- {
- const int InsertAtOnce = 10;
-
- var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
- for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
- {
- var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
- {
- insertText.Append('@')
- .Append(column)
- .Append(i)
- .Append(',');
- }
-
- insertText.Length -= 1;
-
- insertText.Append("),");
- }
-
- insertText.Length--;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var attachment = attachments[i];
-
- statement.TryBind("@AttachmentIndex" + index, attachment.Index);
- statement.TryBind("@Codec" + index, attachment.Codec);
- statement.TryBind("@CodecTag" + index, attachment.CodecTag);
- statement.TryBind("@Comment" + index, attachment.Comment);
- statement.TryBind("@Filename" + index, attachment.FileName);
- statement.TryBind("@MIMEType" + index, attachment.MimeType);
- }
-
- statement.ExecuteNonQuery();
- }
-
- insertText.Length = _mediaAttachmentInsertPrefix.Length;
- }
- }
-
- ///
- /// Gets the attachment.
- ///
- /// The reader.
- /// MediaAttachment.
- private MediaAttachment GetMediaAttachment(SqliteDataReader reader)
- {
- var item = new MediaAttachment
- {
- Index = reader.GetInt32(1)
- };
-
- if (reader.TryGetString(2, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(3, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(4, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(5, out var fileName))
- {
- item.FileName = fileName;
- }
-
- if (reader.TryGetString(6, out var mimeType))
- {
- item.MimeType = mimeType;
- }
-
- return item;
- }
-
- private static string BuildMediaAttachmentInsertPrefix()
- {
- var queryPrefixText = new StringBuilder();
- queryPrefixText.Append("insert into mediaattachments (");
- foreach (var column in _mediaAttachmentSaveColumns)
- {
- queryPrefixText.Append(column)
- .Append(',');
- }
-
- queryPrefixText.Length -= 1;
- queryPrefixText.Append(") values ");
- return queryPrefixText.ToString();
- }
-
-#nullable enable
-
- private readonly struct QueryTimeLogger : IDisposable
- {
- private readonly ILogger _logger;
- private readonly string _commandText;
- private readonly string _methodName;
- private readonly long _startTimestamp;
-
- public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "")
- {
- _logger = logger;
- _commandText = commandText;
- _methodName = methodName;
- _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1;
- }
-
- public void Dispose()
- {
- if (_startTimestamp == -1)
- {
- return;
- }
-
- var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds;
-
-#if DEBUG
- const int SlowThreshold = 100;
-#else
- const int SlowThreshold = 10;
-#endif
-
- if (elapsedMs >= SlowThreshold)
- {
- _logger.LogDebug(
- "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
- _methodName,
- elapsedMs,
- _commandText);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
deleted file mode 100644
index bfdcc08f42..0000000000
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ /dev/null
@@ -1,369 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
- {
- private readonly IUserManager _userManager;
-
- public SqliteUserDataRepository(
- ILogger logger,
- IServerConfigurationManager config,
- IUserManager userManager)
- : base(logger)
- {
- _userManager = userManager;
-
- DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
- }
-
- ///
- /// Opens the connection to the database.
- ///
- public override void Initialize()
- {
- base.Initialize();
-
- using (var connection = GetConnection())
- {
- var userDatasTableExists = TableExists(connection, "UserDatas");
- var userDataTableExists = TableExists(connection, "userdata");
-
- var users = userDatasTableExists ? null : _userManager.Users;
- using var transaction = connection.BeginTransaction();
- connection.Execute(string.Join(
- ';',
- "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
- "drop index if exists idx_userdata",
- "drop index if exists idx_userdata1",
- "drop index if exists idx_userdata2",
- "drop index if exists userdataindex1",
- "drop index if exists userdataindex",
- "drop index if exists userdataindex3",
- "drop index if exists userdataindex4",
- "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
- "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
- "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
- "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
-
- if (!userDataTableExists)
- {
- transaction.Commit();
- return;
- }
-
- var existingColumnNames = GetColumnNames(connection, "userdata");
-
- AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
- AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
- AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
-
- if (userDatasTableExists)
- {
- return;
- }
-
- ImportUserIds(connection, users);
-
- connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
-
- transaction.Commit();
- }
- }
-
- private void ImportUserIds(ManagedConnection db, IEnumerable users)
- {
- var userIdsWithUserData = GetAllUserIdsWithUserData(db);
-
- using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
- {
- foreach (var user in users)
- {
- if (!userIdsWithUserData.Contains(user.Id))
- {
- continue;
- }
-
- statement.TryBind("@UserId", user.Id);
- statement.TryBind("@InternalUserId", user.InternalId);
-
- statement.ExecuteNonQuery();
- }
- }
- }
-
- private List GetAllUserIdsWithUserData(ManagedConnection db)
- {
- var list = new List();
-
- using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- try
- {
- list.Add(row.GetGuid(0));
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error while getting user");
- }
- }
- }
-
- return list;
- }
-
- ///
- public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- PersistUserData(userId, key, userData, cancellationToken);
- }
-
- ///
- public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- PersistAllUserData(userId, userData, cancellationToken);
- }
-
- ///
- /// Persists the user data.
- ///
- /// The user id.
- /// The key.
- /// The user data.
- /// The cancellation token.
- public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- SaveUserData(connection, internalUserId, key, userData);
- transaction.Commit();
- }
- }
-
- private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
- {
- using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
- {
- statement.TryBind("@userId", internalUserId);
- statement.TryBind("@key", key);
-
- if (userData.Rating.HasValue)
- {
- statement.TryBind("@rating", userData.Rating.Value);
- }
- else
- {
- statement.TryBindNull("@rating");
- }
-
- statement.TryBind("@played", userData.Played);
- statement.TryBind("@playCount", userData.PlayCount);
- statement.TryBind("@isFavorite", userData.IsFavorite);
- statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
-
- if (userData.LastPlayedDate.HasValue)
- {
- statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
- }
- else
- {
- statement.TryBindNull("@lastPlayedDate");
- }
-
- if (userData.AudioStreamIndex.HasValue)
- {
- statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@AudioStreamIndex");
- }
-
- if (userData.SubtitleStreamIndex.HasValue)
- {
- statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@SubtitleStreamIndex");
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- ///
- /// Persist all user data for the specified user.
- ///
- private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- foreach (var userItemData in userDataList)
- {
- SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
- }
-
- transaction.Commit();
- }
- }
-
- ///
- /// Gets the user data.
- ///
- /// The user id.
- /// The key.
- /// Task{UserItemData}.
- ///
- /// userId
- /// or
- /// key.
- ///
- public UserItemData GetUserData(long userId, string key)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- using (var connection = GetConnection(true))
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
- statement.TryBind("@Key", key);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return ReadRow(row);
- }
- }
-
- return null;
- }
- }
-
- public UserItemData GetUserData(long userId, List keys)
- {
- ArgumentNullException.ThrowIfNull(keys);
-
- if (keys.Count == 0)
- {
- return null;
- }
-
- return GetUserData(userId, keys[0]);
- }
-
- ///
- /// Return all user-data associated with the given user.
- ///
- /// The internal user id.
- /// The list of user item data.
- public List GetAllUserData(long userId)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- var list = new List();
-
- using (var connection = GetConnection())
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(ReadRow(row));
- }
- }
- }
-
- return list;
- }
-
- ///
- /// Read a row from the specified reader into the provided userData object.
- ///
- /// The list of result set values.
- /// The user item data.
- private UserItemData ReadRow(SqliteDataReader reader)
- {
- var userData = new UserItemData
- {
- Key = reader.GetString(0)
- };
-
- if (reader.TryGetDouble(2, out var rating))
- {
- userData.Rating = rating;
- }
-
- userData.Played = reader.GetBoolean(3);
- userData.PlayCount = reader.GetInt32(4);
- userData.IsFavorite = reader.GetBoolean(5);
- userData.PlaybackPositionTicks = reader.GetInt64(6);
-
- if (reader.TryReadDateTime(7, out var lastPlayedDate))
- {
- userData.LastPlayedDate = lastPlayedDate;
- }
-
- if (reader.TryGetInt32(8, out var audioStreamIndex))
- {
- userData.AudioStreamIndex = audioStreamIndex;
- }
-
- if (reader.TryGetInt32(9, out var subtitleStreamIndex))
- {
- userData.SubtitleStreamIndex = subtitleStreamIndex;
- }
-
- return userData;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SynchronousMode.cs b/Emby.Server.Implementations/Data/SynchronousMode.cs
deleted file mode 100644
index cde524e2e0..0000000000
--- a/Emby.Server.Implementations/Data/SynchronousMode.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-///
-/// The disk synchronization mode, controls how aggressively SQLite will write data
-/// all the way out to physical storage.
-///
-public enum SynchronousMode
-{
- ///
- /// SQLite continues without syncing as soon as it has handed data off to the operating system.
- ///
- Off = 0,
-
- ///
- /// SQLite database engine will still sync at the most critical moments.
- ///
- Normal = 1,
-
- ///
- /// SQLite database engine will use the xSync method of the VFS
- /// to ensure that all content is safely written to the disk surface prior to continuing.
- ///
- Full = 2,
-
- ///
- /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
- /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
- ///
- Extra = 3
-}
diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs
deleted file mode 100644
index d2427ce478..0000000000
--- a/Emby.Server.Implementations/Data/TempStoreMode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-///
-/// Storage mode used by temporary database files.
-///
-public enum TempStoreMode
-{
- ///
- /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
- /// is used to determine where temporary tables and indices are stored.
- ///
- Default = 0,
-
- ///
- /// Temporary tables and indices are stored in a file.
- ///
- File = 1,
-
- ///
- /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
- ///
- Memory = 2
-}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index 2459178d81..0b3c3bbd4f 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -4,6 +4,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger _logger;
- private readonly object _syncLock = new object();
+ private readonly Lock _syncLock = new();
private string? _id;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0c0ba74533..356d1e437a 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
+ private readonly IChapterRepository _chapterRepository;
public DtoService(
ILogger logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy livetvManagerFactory,
- ITrickplayManager trickplayManager)
+ ITrickplayManager trickplayManager,
+ IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
+ _chapterRepository = chapterRepository;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static IList GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
+ private static IReadOnlyList GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
@@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList taggedItems)
+ private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList taggedItems)
{
if (item is MusicArtist)
{
@@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters))
{
- dto.Chapters = _itemRepo.GetChapters(item);
+ dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay))
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 34276355a7..70dd5eb9ae 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -37,7 +37,7 @@
- net8.0
+ net9.0
false
true
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 4c668379c8..fb0a55135f 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly IUserManager _userManager;
private readonly ILogger _logger;
- private readonly object _libraryChangedSyncLock = new();
+ private readonly Lock _libraryChangedSyncLock = new();
private readonly List _foldersAddedTo = new();
private readonly List _foldersRemovedFrom = new();
private readonly List _itemsAdded = new();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index aef02ce6bf..fc174b7c14 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserManager _userManager;
private readonly Dictionary> _changedItems = new();
- private readonly object _syncLock = new();
+ private readonly Lock _syncLock = new();
private Timer? _updateTimer;
@@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
+ if (dto is null)
+ {
+ return null!;
+ }
+
dto.ItemId = i.Id;
return dto;
})
+ .Where(e => e is not null)
.ToArray()
};
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index cb6f7e1d35..a720c86fb2 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State;
///
- public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
///
- public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
///
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
return ret;
}
- private Task SendKeepAliveResponse()
+ private async Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
- return SendAsync(
+ await SendAsync(
new OutboundKeepAliveMessage(),
- CancellationToken.None);
+ CancellationToken.None).ConfigureAwait(false);
}
///
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 774d3563cb..cb5b3993b8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Processes the web socket message received.
///
/// The result.
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
}
- return Task.WhenAll(tasks);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index e75cab64c9..7378cf8851 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
- private readonly List _affectedPaths = new List();
- private readonly object _timerLock = new object();
+ private readonly List _affectedPaths = new();
+ private readonly Lock _timerLock = new();
private Timer? _timer;
private bool _disposed;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4b68f21d55..66b7839f77 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
+ catch (IOException ex)
+ {
+ // IOException generally means the file is not accessible due to filesystem issues
+ // Catch this exception and mark the file as not exist to ignore it
+ _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
+ result.Exists = false;
+ }
}
}
@@ -561,7 +568,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
///
public virtual IEnumerable GetFileSystemEntries(string path, bool recursive = false)
{
+ // Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
+ // But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
+ // For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -618,7 +628,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 82db7c46b3..8b28691498 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -116,13 +117,12 @@ namespace Emby.Server.Implementations.Images
var mimeType = MimeTypes.GetMimeType(outputPath);
- if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
- mimeType = "image/png";
+ mimeType = MediaTypeNames.Image.Png;
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
- File.Delete(outputPath);
return ItemUpdateType.ImageUpdate;
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 28f7ed6598..c483f3c61f 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -76,13 +76,14 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
+ private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
///
/// The _root folder sync lock.
///
- private readonly object _rootFolderSyncLock = new object();
- private readonly object _userRootFolderSyncLock = new object();
+ private readonly Lock _rootFolderSyncLock = new();
+ private readonly Lock _userRootFolderSyncLock = new();
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
@@ -112,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// The image processor.
/// The naming options.
/// The directory service.
+ /// The People Repository.
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IPeopleRepository peopleRepository)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger();
@@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library
_imageProcessor = imageProcessor;
_cache = new ConcurrentDictionary();
_namingOptions = namingOptions;
-
+ _peopleRepository = peopleRepository;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -751,14 +754,7 @@ namespace Emby.Server.Implementations.Library
if (folder.Id.IsEmpty())
{
- if (string.IsNullOrEmpty(folder.Path))
- {
- folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
- }
- else
- {
- folder.Id = GetNewItemId(folder.Path, folder.GetType());
- }
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
}
var dbItem = GetItemById(folder.Id) as BasePluginFolder;
@@ -1053,9 +1049,17 @@ namespace Emby.Server.Implementations.Library
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
- foreach (var folder in GetUserRootFolder().Children.OfType())
+ foreach (var child in GetUserRootFolder().Children.OfType())
{
- await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ // If the user has somehow deleted the collection directory, remove the metadata from the database.
+ if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
+ {
+ _itemRepository.DeleteItem(collectionFolder.Id);
+ }
+ else
+ {
+ await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -1274,7 +1278,7 @@ namespace Emby.Server.Implementations.Library
return ItemIsVisible(item, user) ? item : null;
}
- public List GetItemList(InternalItemsQuery query, bool allowExternalContent)
+ public IReadOnlyList GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
@@ -1300,7 +1304,7 @@ namespace Emby.Server.Implementations.Library
return itemList;
}
- public List GetItemList(InternalItemsQuery query)
+ public IReadOnlyList GetItemList(InternalItemsQuery query)
{
return GetItemList(query, true);
}
@@ -1324,7 +1328,7 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
- public List GetItemList(InternalItemsQuery query, List parents)
+ public IReadOnlyList GetItemList(InternalItemsQuery query, List parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1357,7 +1361,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
- public List GetItemIds(InternalItemsQuery query)
+ public IReadOnlyList GetItemIds(InternalItemsQuery query)
{
if (query.User is not null)
{
@@ -1955,13 +1959,13 @@ namespace Emby.Server.Implementations.Library
///
public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
+ _itemRepository.SaveItems(items, cancellationToken);
+
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
- _itemRepository.SaveItems(items, cancellationToken);
-
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -2736,12 +2740,12 @@ namespace Emby.Server.Implementations.Library
return path;
}
- public List GetPeople(InternalPeopleQuery query)
+ public IReadOnlyList GetPeople(InternalPeopleQuery query)
{
- return _itemRepository.GetPeople(query);
+ return _peopleRepository.GetPeople(query);
}
- public List GetPeople(BaseItem item)
+ public IReadOnlyList GetPeople(BaseItem item)
{
if (item.SupportsPeople)
{
@@ -2756,12 +2760,12 @@ namespace Emby.Server.Implementations.Library
}
}
- return new List();
+ return [];
}
- public List GetPeopleItems(InternalPeopleQuery query)
+ public IReadOnlyList GetPeopleItems(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query)
+ return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
try
@@ -2779,9 +2783,9 @@ namespace Emby.Server.Implementations.Library
.ToList()!; // null values are filtered out
}
- public List GetPeopleNames(InternalPeopleQuery query)
+ public IReadOnlyList GetPeopleNames(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query);
+ return _peopleRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List people)
@@ -2790,16 +2794,17 @@ namespace Emby.Server.Implementations.Library
}
///
- public async Task UpdatePeopleAsync(BaseItem item, List people, CancellationToken cancellationToken)
+ public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
return;
}
- _itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
+ people = people.Where(e => e is not null).ToArray();
+ _peopleRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
@@ -2914,14 +2919,13 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable people, CancellationToken cancellationToken)
{
- List? personsToSave = null;
-
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
+ var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
@@ -2938,6 +2942,7 @@ namespace Emby.Server.Implementations.Library
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
+ createEntity = true;
}
foreach (var id in person.ProviderIds)
@@ -2965,15 +2970,15 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- (personsToSave ??= new()).Add(personEntity);
+ if (createEntity)
+ {
+ CreateItems([personEntity], null, CancellationToken.None);
+ }
+
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ CreateItems([personEntity], null, CancellationToken.None);
}
}
-
- if (personsToSave is not null)
- {
- CreateItems(personsToSave, null, CancellationToken.None);
- }
}
private void StartScanInBackground()
@@ -3027,7 +3032,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
- libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
+ libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 90a01c052c..5795c47ccc 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -38,7 +39,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
+ private const char LiveStreamIdDelimiter = '_';
private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
@@ -51,7 +52,8 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
-
+ private readonly IMediaStreamRepository _mediaStreamRepository;
+ private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -69,7 +71,9 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IMediaStreamRepository mediaStreamRepository,
+ IMediaAttachmentRepository mediaAttachmentRepository)
{
_appHost = appHost;
_itemRepo = itemRepo;
@@ -82,6 +86,8 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
+ _mediaStreamRepository = mediaStreamRepository;
+ _mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable providers)
@@ -89,9 +95,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
- public List GetMediaStreams(MediaStreamQuery query)
+ public IReadOnlyList GetMediaStreams(MediaStreamQuery query)
{
- var list = _itemRepo.GetMediaStreams(query);
+ var list = _mediaStreamRepository.GetMediaStreams(query);
foreach (var stream in list)
{
@@ -121,7 +127,7 @@ namespace Emby.Server.Implementations.Library
return false;
}
- public List GetMediaStreams(Guid itemId)
+ public IReadOnlyList GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@@ -131,7 +137,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
- private List GetMediaStreamsForItem(List streams)
+ private IReadOnlyList GetMediaStreamsForItem(IReadOnlyList streams)
{
foreach (var stream in streams)
{
@@ -145,13 +151,13 @@ namespace Emby.Server.Implementations.Library
}
///
- public List GetMediaAttachments(MediaAttachmentQuery query)
+ public IReadOnlyList GetMediaAttachments(MediaAttachmentQuery query)
{
- return _itemRepo.GetMediaAttachments(query);
+ return _mediaAttachmentRepository.GetMediaAttachments(query);
}
///
- public List GetMediaAttachments(Guid itemId)
+ public IReadOnlyList GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@@ -159,7 +165,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -212,7 +218,7 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
- return SortMediaSources(list);
+ return SortMediaSources(list).ToArray();
}
/// >
@@ -307,7 +313,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
- var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
@@ -332,7 +338,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
- public List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
+ public IReadOnlyList GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@@ -453,7 +459,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static List SortMediaSources(IEnumerable sources)
+ private static IEnumerable SortMediaSources(IEnumerable sources)
{
return sources.OrderBy(i =>
{
@@ -470,8 +476,7 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
- .Where(i => i.Type != MediaSourceType.Placeholder)
- .ToList();
+ .Where(i => i.Type != MediaSourceType.Placeholder);
}
public async Task> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -806,7 +811,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
- public async Task> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+ public async Task> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@@ -829,10 +834,7 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
- return new List
- {
- stream
- };
+ return [stream];
}
public async Task CloseLiveStream(string id)
@@ -864,11 +866,11 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(key);
- var keys = key.Split(LiveStreamIdDelimeter, 2);
+ var keys = key.Split(LiveStreamIdDelimiter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
return (provider, keyId);
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index a69a0f33f3..71c69ec50a 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -24,30 +25,23 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
- public List GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- var list = new List
- {
- item
- };
-
- list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
-
- return list;
+ return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
///
- public List GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
- public List GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
@@ -63,12 +57,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
- public List GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromGenres(IEnumerable genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -97,7 +91,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public List GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a03c1214d6..14798dda65 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
- // It's a boxset if the path is a directory with [playlist] in its name
+ // It's a playlist if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 7f3f8615e2..3ac1d02192 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library
}
};
- List mediaItems;
+ IReadOnlyList mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 320685b1f1..76e564d535 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
///
public Task Run(IProgress progress, CancellationToken cancellationToken)
{
- var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
- var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
+ var posters = GetItemsWithImageType(ImageType.Primary)
+ .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
+ var backdrops = GetItemsWithImageType(ImageType.Thumb)
+ .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
- backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
+ backdrops = GetItemsWithImageType(ImageType.Backdrop)
+ .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 62d22b23ff..a41ef888b0 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,17 +1,21 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -26,22 +30,18 @@ namespace Emby.Server.Implementations.Library
new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
- private readonly IUserDataRepository _repository;
+ private readonly IDbContextFactory _repository;
///