diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index e58a2bdc7e..31f861f63f 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: CompatibilityCheck
@@ -34,6 +34,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool'
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index d2c087c14d..1086d51d27 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 5.0.302
+ DotNetSdkVersion: 6.0.x
jobs:
- job: Build
@@ -54,6 +54,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Publish Server'
@@ -91,3 +92,10 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish Artifact Extensions'
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+ artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 543fd7fc6d..4abe52b43b 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -195,10 +195,11 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Use .NET 5.0 sdk'
+ displayName: 'Use .NET 6.0 sdk'
inputs:
packageType: 'sdk'
- version: '5.0.x'
+ version: '6.0.x'
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -211,6 +212,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@@ -225,6 +227,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 7ec4cdad1d..80a5732eee 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: Test
@@ -41,6 +41,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud'
@@ -94,5 +95,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 4e8b6557b9..19c9caacbb 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -5,8 +5,6 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
- value: 5.0.302
pr:
autoCancel: true
@@ -57,6 +55,9 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
+ Extensions:
+ NugetPackageName: Jellyfin.Extensions
+ AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3e456f9093..e07d913b5a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,9 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.x'
+ dotnet-version: '6.0.x'
+ include-prerelease: true
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index e0b91ecee6..af4d8beb93 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@1.4
+ uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
new file mode 100644
index 0000000000..ea9188f1b1
--- /dev/null
+++ b/.github/workflows/openapi.yml
@@ -0,0 +1,126 @@
+name: OpenAPI
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+
+jobs:
+ openapi-head:
+ name: OpenAPI - HEAD
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ include-prerelease: true
+ - 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@v2
+ with:
+ name: openapi-head
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-base:
+ name: OpenAPI - BASE
+ if: ${{ github.base_ref != '' }}
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.base_ref }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ include-prerelease: true
+ - 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@v2
+ with:
+ name: openapi-base
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-diff:
+ name: OpenAPI - Difference
+ if: ${{ github.event_name == 'pull_request_target' }}
+ runs-on: ubuntu-latest
+ needs:
+ - openapi-head
+ - openapi-base
+ steps:
+ - name: Download openapi-head
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-head
+ path: openapi-head
+ - name: Download openapi-base
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-base
+ path: openapi-base
+ - name: Workaround openapi-diff issue
+ run: |
+ sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
+ sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
+ - name: Calculate OpenAPI difference
+ uses: docker://openapitools/openapi-diff
+ continue-on-error: true
+ with:
+ args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
+ - id: read-diff
+ name: Read openapi-diff output
+ run: |
+ body=$(cat openapi-changes.md)
+ body="${body//'%'/'%25'}"
+ body="${body//$'\n'/'%0A'}"
+ body="${body//$'\r'/'%0D'}"
+ echo ::set-output name=body::$body
+ - name: Find difference comment
+ uses: peter-evans/find-comment@v1
+ id: find-comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ direction: last
+ body-includes: openapi-diff-workflow-comment
+ - name: Reply or edit difference comment (changed)
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ steps.read-diff.outputs.body != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+
+
+ Changes in OpenAPI specification found. Expand to see details.
+
+ ${{ steps.read-diff.outputs.body }}
+
+
+ - name: Edit difference comment (unchanged)
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+
+
+ No changes to OpenAPI specification found. See history of this comment for previous changes.
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e55ea22485..b82956a721 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/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.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/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index cb52cafedf..5c4a031bc1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -149,6 +149,7 @@
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 3190fec5c5..e133c08193 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,8 @@
-ARG DOTNET_VERSION=5.0
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@@ -8,7 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-FROM debian:bullseye-slim as app
+FROM debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -18,15 +22,16 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=20.3.2
-ARG IGC_VERSION=1.0.5435
-ARG NEO_VERSION=20.46.18421
-ARG LEVEL_ZERO_VERSION=1.0.18421
+ARG GMMLIB_VERSION=21.2.1
+ARG IGC_VERSION=1.0.8517
+ARG NEO_VERSION=21.35.20826
+ARG LEVEL_ZERO_VERSION=1.2.20826
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
+# curl: healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
@@ -57,7 +62,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -72,6 +77,8 @@ RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -81,3 +88,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index dcd006ff83..a46fa331df 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:bullseye-slim as app
+FROM arm32v7/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
+
+# curl: setup & healthcheck
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -42,7 +44,7 @@ RUN apt-get update \
vainfo \
libva2 \
locales \
- && apt-get remove curl gnupg -y \
+ && apt-get remove gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -50,7 +52,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -66,6 +68,8 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -75,3 +79,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 7311c6b9ff..1279c47f8e 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:bullseye-slim as app
+FROM arm64v8/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
+
+# curl: healcheck
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
ffmpeg \
libssl-dev \
@@ -33,6 +35,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
libomxil-bellagio0 \
libomxil-bellagio-bin \
locales \
+ curl \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -40,7 +43,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -56,6 +59,8 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -65,3 +70,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index b8301e2f27..755d29160c 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,7 +10,7 @@
- net5.0
+ net6.0
false
true
AllDisabledByDefault
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index b4a11ed5d6..7f8ece47dc 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList allFiles)
{
- var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
+ var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index c000784997..0a84f30c4c 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
@@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (filter.Contains("res@resolution"))
@@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (totalBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetVideoMediaProfile(
@@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetAudioBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(
@@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
- writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
+ writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
var clientId = GetClientId(folder, stubType);
@@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue)
{
- AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
if (item is Episode)
{
- AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
}
}
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index af70793ccf..f37d2d7d7b 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -359,14 +359,17 @@ namespace Emby.Dlna
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
{
+ var length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path);
- if (!fileInfo.Exists || fileInfo.Length != stream.Length)
+ if (!fileInfo.Exists || fileInfo.Length != length)
{
Directory.CreateDirectory(systemProfilesPath);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.PreallocationSize = length;
+ using (var fileStream = new FileStream(path, fileOptions))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -413,7 +416,7 @@ namespace Emby.Dlna
}
///
- public void UpdateProfile(DeviceProfile profile)
+ public void UpdateProfile(string profileId, DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -427,7 +430,7 @@ namespace Emby.Dlna
throw new ArgumentException("Profile is missing Name");
}
- var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
+ var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename);
@@ -486,18 +489,22 @@ namespace Emby.Dlna
}
///
- public ImageStream GetIcon(string filename)
+ public ImageStream? GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
-
- return new ImageStream
+ var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream == null)
{
- Format = format,
- Stream = _assembly.GetManifestResourceStream(resource)
+ return null;
+ }
+
+ return new ImageStream(stream)
+ {
+ Format = format
};
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 970c16d2e6..c8332e44e4 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,10 +17,9 @@
- net5.0
+ net6.0
false
true
- AllDisabledByDefault
@@ -73,7 +72,7 @@
-
+
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 3c91360904..d17e238715 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -25,8 +26,6 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
@@ -82,9 +81,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-')[^1];
-
- if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return val;
}
@@ -107,7 +104,7 @@ namespace Emby.Dlna.Eventing
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
- response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
+ response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
return response;
}
@@ -164,7 +161,7 @@ namespace Emby.Dlna.Eventing
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
- options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
try
{
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 11fcd81cff..0b22880003 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- Volume = int.Parse(volumeValue, UsCulture);
+ Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
if (Volume > 0)
{
@@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Duration = TimeSpan.Parse(duration, UsCulture);
+ Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
}
else
{
@@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Position = TimeSpan.Parse(position, UsCulture);
+ Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
}
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
- var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
- var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
+ var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
return new DeviceIcon
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 0e49fd2c02..f25d8017ef 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlayToController : ISessionController, IDisposable
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index f14f73bb6f..cade7b4c2c 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -45,10 +43,12 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
@@ -78,14 +78,15 @@ namespace Emby.Dlna.PlayTo
{
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
- options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+ options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
+ options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
- options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
}
public async Task GetDataAsync(string url, CancellationToken cancellationToken)
@@ -94,12 +95,13 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 3f3dfccd3a..80a45f2b25 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -15,7 +15,6 @@ namespace Emby.Dlna.Server
{
private readonly DeviceProfile _profile;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
private readonly string _serverAddress;
private readonly string _serverName;
@@ -193,10 +192,10 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
.Append("");
builder.Append("")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
@@ -250,8 +249,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
- // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
- return SecurityElement.Escape(url) ?? string.Empty;
+ return SecurityElement.Escape(url);
}
private IEnumerable GetIcons()
diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
index a97c4d63a6..68fd987585 100644
--- a/Emby.Dlna/Service/BaseService.cs
+++ b/Emby.Dlna/Service/BaseService.cs
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
return EventManager.CancelEventSubscription(subscriptionId);
}
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
+ return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
}
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
+ return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
}
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index baf350c6f1..149e4b5d91 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,10 +6,9 @@
- net5.0
+ net6.0
false
true
- AllDisabledByDefault
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 7d952aa23b..ac73cfa421 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -102,7 +102,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+ using (var fileStream = AsyncFile.OpenRead(file.Item1))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 915ce42cc9..5ddcf37fe6 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -137,8 +137,11 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
- @"(\[.*\])"
+ @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"^(?.+?)(\[.*\])",
+ @"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
+ @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)",
+ @"^\s*(?.+?)\s+-\s+[0-9]+\s*$"
};
SubtitleFileExtensions = new[]
@@ -250,6 +253,8 @@ namespace Emby.Naming.Common
},
//
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
+ //
+ new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?[0-9]{4})[\\.-](?[0-9]{2})[\\.-](?[0-9]{2})", true)
{
DateTimeFormats = new[]
@@ -368,6 +373,20 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
+
+ // Series and season only expression
+ // "the show/season 1", "the show/s01"
+ new EpisodeExpression(@"(.*(\\|\/))*(?.+)\/[Ss](eason)?[\. _\-]*(?[0-9]+)")
+ {
+ IsNamed = true
+ },
+
+ // Series and season only expression
+ // "the show S01", "the show season 1"
+ new EpisodeExpression(@"(.*(\\|\/))*(?.+)[\. _\-]+[sS](eason)?[\. _\-]*(?[0-9]+)")
+ {
+ IsNamed = true
+ },
};
EpisodeWithoutSeasonExpressions = new[]
@@ -478,6 +497,12 @@ namespace Emby.Naming.Common
"-deleted",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 07d879e96a..e9e9edda65 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,14 +6,13 @@
- net5.0
+ net6.0
false
true
true
true
true
snupkg
- AllDisabledByDefault
diff --git a/Emby.Naming/TV/SeriesInfo.cs b/Emby.Naming/TV/SeriesInfo.cs
new file mode 100644
index 0000000000..5d6cb4bd37
--- /dev/null
+++ b/Emby.Naming/TV/SeriesInfo.cs
@@ -0,0 +1,29 @@
+namespace Emby.Naming.TV
+{
+ ///
+ /// Holder object for Series information.
+ ///
+ public class SeriesInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to the file.
+ public SeriesInfo(string path)
+ {
+ Path = path;
+ }
+
+ ///
+ /// Gets or sets the path.
+ ///
+ /// The path.
+ public string Path { get; set; }
+
+ ///
+ /// Gets or sets the name of the series.
+ ///
+ /// The name of the series.
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs
new file mode 100644
index 0000000000..a62e5f4d63
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,61 @@
+using System.Globalization;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ ///
+ /// Used to parse information about series from paths containing more information that only the series name.
+ /// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
+ ///
+ public static class SeriesPathParser
+ {
+ ///
+ /// Parses information about series from path.
+ ///
+ /// object containing EpisodeExpressions and MultipleEpisodeExpressions.
+ /// Path.
+ /// Returns object.
+ public static SeriesPathParserResult Parse(NamingOptions options, string path)
+ {
+ SeriesPathParserResult? result = null;
+
+ foreach (var expression in options.EpisodeExpressions)
+ {
+ var currentResult = Parse(path, expression);
+ if (currentResult.Success)
+ {
+ result = currentResult;
+ break;
+ }
+ }
+
+ if (result != null)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
+ }
+ }
+
+ return result ?? new SeriesPathParserResult();
+ }
+
+ private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
+ {
+ var result = new SeriesPathParserResult();
+
+ var match = expression.Regex.Match(name);
+
+ if (match.Success && match.Groups.Count >= 3)
+ {
+ if (expression.IsNamed)
+ {
+ result.SeriesName = match.Groups["seriesname"].Value;
+ result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParserResult.cs b/Emby.Naming/TV/SeriesPathParserResult.cs
new file mode 100644
index 0000000000..44cd2fdfa1
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParserResult.cs
@@ -0,0 +1,19 @@
+namespace Emby.Naming.TV
+{
+ ///
+ /// Holder object for result.
+ ///
+ public class SeriesPathParserResult
+ {
+ ///
+ /// Gets or sets the name of the series.
+ ///
+ /// The name of the series.
+ public string? SeriesName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether parsing was successful.
+ ///
+ public bool Success { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
new file mode 100644
index 0000000000..156a03c9ed
--- /dev/null
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -0,0 +1,49 @@
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ ///
+ /// Used to resolve information about series from path.
+ ///
+ public static class SeriesResolver
+ {
+ ///
+ /// 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".
+ ///
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))");
+
+ ///
+ /// Resolve information about series from path.
+ ///
+ /// object passed to .
+ /// Path to series.
+ /// SeriesInfo.
+ public static SeriesInfo Resolve(NamingOptions options, string path)
+ {
+ string seriesName = Path.GetFileName(path);
+
+ SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
+ if (result.Success)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ seriesName = result.SeriesName;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(seriesName))
+ {
+ seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+ }
+
+ return new SeriesInfo(path)
+ {
+ Name = seriesName
+ };
+ }
+ }
+}
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 4eef3ebc5e..b813335008 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -17,38 +17,39 @@ namespace Emby.Naming.Video
/// List of regex to parse name and year from.
/// Parsing result string.
/// True if parsing was successful.
- public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out ReadOnlySpan newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out string newName)
{
if (string.IsNullOrEmpty(name))
{
- newName = ReadOnlySpan.Empty;
+ newName = string.Empty;
return false;
}
- var len = expressions.Count;
- for (int i = 0; i < len; i++)
+ // Iteratively apply the regexps to clean the string.
+ bool cleaned = false;
+ for (int i = 0; i < expressions.Count; i++)
{
if (TryClean(name, expressions[i], out newName))
{
- return true;
+ cleaned = true;
+ name = newName;
}
}
- newName = ReadOnlySpan.Empty;
- return false;
+ newName = cleaned ? name : string.Empty;
+ return cleaned;
}
- private static bool TryClean(string name, Regex expression, out ReadOnlySpan newName)
+ private static bool TryClean(string name, Regex expression, out string newName)
{
var match = expression.Match(name);
- int index = match.Index;
- if (match.Success && index != 0)
+ if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{
- newName = name.AsSpan().Slice(0, match.Index);
+ newName = cleaned.Value;
return true;
}
- newName = ReadOnlySpan.Empty;
+ newName = string.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index a32af002cc..7bc226614e 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -11,6 +11,7 @@ namespace Emby.Naming.Video
///
public class ExtraResolver
{
+ private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private readonly NamingOptions _options;
///
@@ -62,9 +63,10 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
- if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 3b1d906c64..4c9df27f50 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -87,9 +87,9 @@ namespace Emby.Naming.Video
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, namingOptions, out ReadOnlySpan newName))
+ && TryCleanString(name, namingOptions, out var newName))
{
- name = newName.ToString();
+ name = newName;
}
}
@@ -138,7 +138,7 @@ namespace Emby.Naming.Video
/// The naming options.
/// Clean name.
/// True if cleaning of name was successful.
- public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
{
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 5edcf2f295..d200682e65 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 00b2f0f94c..bf6252c195 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
- net5.0
+ net6.0
false
true
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0308a68e42..3a916e5c01 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -41,20 +41,19 @@ namespace Emby.Server.Implementations.AppBase
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- byte[] newBytes = stream.GetBuffer();
- int newBytesLen = (int)stream.Length;
+ Span newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
+ if (buffer == null || !newBytes.SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+
// Save it after load in case we got new items
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
- fs.Write(newBytes, 0, newBytesLen);
+ fs.Write(newBytes);
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index d507c3fd19..7da0e2f21e 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -147,25 +147,20 @@ namespace Emby.Server.Implementations
/// Instance of the interface.
/// Instance of the interface.
/// The interface.
- /// Instance of the interface.
- /// Instance of the interface.
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startupConfig,
- IFileSystem fileSystem,
- IServiceCollection serviceCollection)
+ IConfiguration startupConfig)
{
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
- _fileSystemManager = fileSystem;
- ServiceCollection = serviceCollection;
+ _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger(), applicationPaths);
Logger = LoggerFactory.CreateLogger();
- fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
+ _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -230,8 +225,6 @@ namespace Emby.Server.Implementations
///
protected ILogger Logger { get; }
- protected IServiceCollection ServiceCollection { get; }
-
///
/// Gets the logger factory.
///
@@ -306,7 +299,7 @@ namespace Emby.Server.Implementations
///
public string Name => ApplicationProductName;
- private CertificateInfo CertificateInfo { get; set; }
+ private string CertificatePath { get; set; }
public X509Certificate2 Certificate { get; private set; }
@@ -521,7 +514,7 @@ namespace Emby.Server.Implementations
}
///
- public void Init()
+ public void Init(IServiceCollection serviceCollection)
{
DiscoverTypes();
@@ -548,135 +541,132 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
- CertificateInfo = new CertificateInfo
- {
- Path = networkConfiguration.CertificatePath,
- Password = networkConfiguration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
+ CertificatePath = networkConfiguration.CertificatePath;
+ Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
- RegisterServices();
+ RegisterServices(serviceCollection);
- _pluginManager.RegisterServices(ServiceCollection);
+ _pluginManager.RegisterServices(serviceCollection);
}
///
/// Registers services/resources with the service collection that will be available via DI.
///
- protected virtual void RegisterServices()
+ /// Instance of the interface.
+ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
- ServiceCollection.AddSingleton(_startupOptions);
+ serviceCollection.AddSingleton(_startupOptions);
- ServiceCollection.AddMemoryCache();
+ serviceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(ConfigurationManager);
- ServiceCollection.AddSingleton(ConfigurationManager);
- ServiceCollection.AddSingleton(this);
- ServiceCollection.AddSingleton(_pluginManager);
- ServiceCollection.AddSingleton(ApplicationPaths);
+ serviceCollection.AddSingleton(ConfigurationManager);
+ serviceCollection.AddSingleton(ConfigurationManager);
+ serviceCollection.AddSingleton(this);
+ serviceCollection.AddSingleton(_pluginManager);
+ serviceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(NetManager);
+ serviceCollection.AddSingleton(NetManager);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(_xmlSerializer);
+ serviceCollection.AddSingleton(_xmlSerializer);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(this);
- ServiceCollection.AddSingleton(ApplicationPaths);
+ serviceCollection.AddSingleton(this);
+ serviceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddSingleton();
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddSingleton();
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddScoped();
+ serviceCollection.AddScoped();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddScoped();
- ServiceCollection.AddScoped();
- ServiceCollection.AddScoped();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
}
///
@@ -729,30 +719,27 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
- private X509Certificate2 GetCertificate(CertificateInfo info)
+ private X509Certificate2 GetCertificate(string path, string password)
{
- var certificateLocation = info?.Path;
-
- if (string.IsNullOrWhiteSpace(certificateLocation))
+ if (string.IsNullOrWhiteSpace(path))
{
return null;
}
try
{
- if (!File.Exists(certificateLocation))
+ if (!File.Exists(path))
{
return null;
}
// Don't use an empty string password
- var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
+ password = string.IsNullOrWhiteSpace(password) ? null : password;
- var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
- // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
+ var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
- Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
+ Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
return null;
}
@@ -760,7 +747,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
+ Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
return null;
}
}
@@ -882,7 +869,7 @@ namespace Emby.Server.Implementations
"http://" + i + ":" + HttpPort + "/"
};
- if (CertificateInfo != null)
+ if (Certificate != null)
{
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
}
@@ -946,7 +933,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
- && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
+ && !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
@@ -1074,9 +1061,9 @@ namespace Emby.Server.Implementations
///
/// Gets the system status.
///
- /// Where this request originated.
+ /// Where this request originated.
/// SystemInfo.
- public SystemInfo GetSystemInfo(IPAddress source)
+ public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
@@ -1098,7 +1085,7 @@ namespace Emby.Server.Implementations
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(source),
+ LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
@@ -1110,7 +1097,7 @@ namespace Emby.Server.Implementations
.Select(i => new WakeOnLanInfo(i))
.ToList();
- public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
@@ -1119,7 +1106,7 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(address),
+ LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@@ -1141,6 +1128,18 @@ namespace Emby.Server.Implementations
///
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
+ // Return the host in the HTTP request as the API url
+ if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
+ {
+ int? requestPort = request.Host.Port;
+ if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
+ {
+ requestPort = -1;
+ }
+
+ return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
+ }
+
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
@@ -1276,11 +1275,4 @@ namespace Emby.Server.Implementations
_disposed = true;
}
}
-
- internal class CertificateInfo
- {
- public string Path { get; set; }
-
- public string Password { get; set; }
- }
}
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
index 591ae547d6..9e1d550ebc 100644
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ b/Emby.Server.Implementations/Archiving/ZipClient.cs
@@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Archiving
options.Overwrite = true;
}
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -58,6 +59,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -71,6 +73,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -120,6 +123,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -151,6 +155,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 41d1f9b392..09aee602af 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -10,8 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -586,7 +586,7 @@ namespace Emby.Server.Implementations.Channels
{
var supportsLatest = provider is ISupportsLatestMedia;
- return new ChannelFeatures
+ return new ChannelFeatures(channel.Name, channel.Id)
{
CanFilter = !features.MaxPageSize.HasValue,
CanSearch = provider is ISearchableChannel,
@@ -596,8 +596,6 @@ namespace Emby.Server.Implementations.Channels
MediaTypes = features.MediaTypes.ToArray(),
SupportsSortOrderToggle = features.SupportsSortOrderToggle,
SupportsLatestMedia = supportsLatest,
- Name = channel.Name,
- Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
SupportsContentDownloading = features.SupportsContentDownloading,
AutoRefreshLevels = features.AutoRefreshLevels
};
@@ -815,7 +813,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -838,7 +836,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 4a9b280852..673810c49d 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -10,8 +10,12 @@ namespace Emby.Server.Implementations.Cryptography
///
/// Class providing abstractions over cryptographic functions.
///
- public class CryptographyProvider : ICryptoProvider, IDisposable
+ public class CryptographyProvider : ICryptoProvider
{
+ // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
+ // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
+ // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
+ // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
private static readonly HashSet _supportedHashMethods = new HashSet()
{
"MD5",
@@ -30,22 +34,6 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512"
};
- private RandomNumberGenerator _randomNumberGenerator;
-
- private bool _disposed;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public CryptographyProvider()
- {
- // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
- // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
- // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
- // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
- _randomNumberGenerator = RandomNumberGenerator.Create();
- }
-
///
public string DefaultHashMethod => "PBKDF2";
@@ -101,36 +89,6 @@ namespace Emby.Server.Implementations.Cryptography
///
public byte[] GenerateSalt(int length)
- {
- byte[] salt = new byte[length];
- _randomNumberGenerator.GetBytes(salt);
- return salt;
- }
-
- ///
- 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 disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _randomNumberGenerator.Dispose();
- }
-
- _disposed = true;
- }
+ => RandomNumberGenerator.GetBytes(length);
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 01c9fbca81..73c31f49d1 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// The write connection.
protected SQLiteDatabaseConnection WriteConnection { get; set; }
- protected ManagedConnection GetConnection(bool _ = false)
+ protected ManagedConnection GetConnection(bool readOnly = false)
{
WriteLock.Wait();
if (WriteConnection != null)
@@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
_disposed = true;
}
}
-
- ///
- /// 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
- }
-
- ///
- /// 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/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index afc8966f9c..44dad5b178 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -9,8 +9,10 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
- private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
+
+ private SQLiteDatabaseConnection? _db;
+
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 3289e76098..381eb92a88 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.None).ToUniversalTime();
+ DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
- if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
+ if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
- result = dateTimeResult.ToUniversalTime();
+ result = dateTimeResult;
return true;
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 30f88c7964..13f1df7c86 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -16,7 +16,6 @@ using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -25,7 +24,6 @@ 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.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -48,6 +46,11 @@ namespace Emby.Server.Implementations.Data
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,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,@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;
@@ -57,6 +60,231 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
+ private readonly ItemFields[] _allItemFields = Enum.GetValues();
+
+ private static readonly string[] _retriveItemColumns =
+ {
+ "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",
+ "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 _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} 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"
+ };
+
+ 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;
+
+ private static readonly HashSet _programTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Program",
+ "TvChannel",
+ "LiveTvProgram",
+ "LiveTvTvChannel"
+ };
+
+ private static readonly HashSet _programExcludeParentTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Series",
+ "Season",
+ "MusicAlbum",
+ "MusicArtist",
+ "PhotoAlbum"
+ };
+
+ private static readonly HashSet _serviceTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "TvChannel",
+ "LiveTvTvChannel"
+ };
+
+ private static readonly HashSet _startDateTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Program",
+ "LiveTvProgram"
+ };
+
+ private static readonly HashSet _seriesTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Book",
+ "AudioBook",
+ "Episode",
+ "Season"
+ };
+
+ private static readonly HashSet _artistExcludeParentTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Series",
+ "Season",
+ "PhotoAlbum"
+ };
+
+ private static readonly HashSet _artistsTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Audio",
+ "MusicAlbum",
+ "MusicVideo",
+ "AudioBook",
+ "AudioPodcast"
+ };
+
+ private static readonly Type[] _knownTypes =
+ {
+ typeof(LiveTvProgram),
+ typeof(LiveTvChannel),
+ typeof(Series),
+ typeof(Audio),
+ typeof(MusicAlbum),
+ typeof(MusicArtist),
+ typeof(MusicGenre),
+ typeof(MusicVideo),
+ typeof(Movie),
+ typeof(Playlist),
+ typeof(AudioBook),
+ typeof(Trailer),
+ typeof(BoxSet),
+ typeof(Episode),
+ typeof(Season),
+ typeof(Series),
+ typeof(Book),
+ typeof(CollectionFolder),
+ typeof(Folder),
+ typeof(Genre),
+ typeof(Person),
+ typeof(Photo),
+ typeof(PhotoAlbum),
+ typeof(Studio),
+ typeof(UserRootFolder),
+ typeof(UserView),
+ typeof(Video),
+ typeof(Year),
+ typeof(Channel),
+ typeof(AggregateFolder)
+ };
+
+ private readonly Dictionary _types = GetTypeMapDictionary();
+
static SqliteItemRepository()
{
var queryPrefixText = new StringBuilder();
@@ -117,6 +345,8 @@ namespace Emby.Server.Implementations.Data
///
/// Opens the connection to the database.
///
+ /// The user data repository.
+ /// The user manager.
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
const string CreateMediaStreamsTableCommand
@@ -156,7 +386,7 @@ namespace Emby.Server.Implementations.Data
"drop index if exists idx_TypedBaseItems",
"drop index if exists idx_mediastreams",
"drop index if exists idx_mediastreams1",
- "drop index if exists idx_"+ChaptersTableName,
+ "drop index if exists idx_" + ChaptersTableName,
"drop index if exists idx_UserDataKeys1",
"drop index if exists idx_UserDataKeys2",
"drop index if exists idx_TypeTopParentId3",
@@ -342,151 +572,12 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
- private static readonly string[] _retriveItemColumns =
- {
- "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",
- "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 _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} 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"
- };
-
- 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;
-
- 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,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,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
///
/// Save a standard item in the repo.
///
/// The item.
/// The cancellation token.
- /// item
+ /// is null.
public void SaveItem(BaseItem item, CancellationToken cancellationToken)
{
if (item == null)
@@ -511,7 +602,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
+ using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
@@ -528,9 +619,7 @@ namespace Emby.Server.Implementations.Data
/// The items.
/// The cancellation token.
///
- /// items
- /// or
- /// cancellationToken
+ /// or is null.
///
public void SaveItems(IEnumerable items, CancellationToken cancellationToken)
{
@@ -1152,7 +1241,7 @@ namespace Emby.Server.Implementations.Data
return null;
}
- if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
+ if (Enum.TryParse(imageType, true, out ImageType type))
{
image.Type = type;
}
@@ -1218,8 +1307,8 @@ namespace Emby.Server.Implementations.Data
///
/// The id.
/// BaseItem.
- /// id
- ///
+ /// is null.
+ /// is .
public BaseItem RetrieveItem(Guid id)
{
if (id == Guid.Empty)
@@ -1573,7 +1662,6 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(index++, out var audioString))
{
- // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
@@ -1612,18 +1700,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index++, out var lockedFields))
{
- IEnumerable GetLockedFields(string s)
+ List fields = null;
+ foreach (var i in lockedFields.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- yield return parsedValue;
- }
+ (fields ??= new List()).Add(parsedValue);
}
}
- item.LockedFields = GetLockedFields(lockedFields).ToArray();
+ item.LockedFields = fields?.ToArray() ?? Array.Empty();
}
}
@@ -1649,18 +1735,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index, out var trailerTypes))
{
- IEnumerable GetTrailerTypes(string s)
+ List types = null;
+ foreach (var i in trailerTypes.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- yield return parsedValue;
- }
+ (types ??= new List()).Add(parsedValue);
}
}
- trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
+ trailer.TrailerTypes = types?.ToArray() ?? Array.Empty();
}
}
@@ -1993,6 +2077,8 @@ namespace Emby.Server.Implementations.Data
///
/// Saves the chapters.
///
+ /// The item id.
+ /// The chapters.
public void SaveChapters(Guid id, IReadOnlyList chapters)
{
CheckDisposed();
@@ -2092,8 +2178,6 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
- private readonly ItemFields[] _allFields = Enum.GetValues();
-
private bool HasField(InternalItemsQuery query, ItemFields name)
{
switch (name)
@@ -2126,23 +2210,6 @@ namespace Emby.Server.Implementations.Data
}
}
- private static readonly HashSet _programExcludeParentTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "MusicAlbum",
- "MusicArtist",
- "PhotoAlbum"
- };
-
- private static readonly HashSet _programTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "TvChannel",
- "LiveTvProgram",
- "LiveTvTvChannel"
- };
-
private bool HasProgramAttributes(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2158,12 +2225,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
}
- private static readonly HashSet _serviceTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "TvChannel",
- "LiveTvTvChannel"
- };
-
private bool HasServiceName(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2179,12 +2240,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
}
- private static readonly HashSet _startDateTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "LiveTvProgram"
- };
-
private bool HasStartDate(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2220,22 +2275,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
- private static readonly HashSet _artistExcludeParentTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "PhotoAlbum"
- };
-
- private static readonly HashSet _artistsTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Audio",
- "MusicAlbum",
- "MusicVideo",
- "AudioBook",
- "AudioPodcast"
- };
-
private bool HasArtistFields(InternalItemsQuery query)
{
if (_artistExcludeParentTypes.Contains(query.ParentType))
@@ -2251,14 +2290,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
}
- private static readonly HashSet _seriesTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "Book",
- "AudioBook",
- "Episode",
- "Season"
- };
-
private bool HasSeriesFields(InternalItemsQuery query)
{
if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@@ -2276,7 +2307,7 @@ namespace Emby.Server.Implementations.Data
private void SetFinalColumnsToSelect(InternalItemsQuery query, List columns)
{
- foreach (var field in _allFields)
+ foreach (var field in _allItemFields)
{
if (!HasField(query, field))
{
@@ -4818,40 +4849,6 @@ namespace Emby.Server.Implementations.Data
return false;
}
- private static readonly Type[] _knownTypes =
- {
- typeof(LiveTvProgram),
- typeof(LiveTvChannel),
- typeof(Series),
- typeof(Audio),
- typeof(MusicAlbum),
- typeof(MusicArtist),
- typeof(MusicGenre),
- typeof(MusicVideo),
- typeof(Movie),
- typeof(Playlist),
- typeof(AudioBook),
- typeof(Trailer),
- typeof(BoxSet),
- typeof(Episode),
- typeof(Season),
- typeof(Series),
- typeof(Book),
- typeof(CollectionFolder),
- typeof(Folder),
- typeof(Genre),
- typeof(Person),
- typeof(Photo),
- typeof(PhotoAlbum),
- typeof(Studio),
- typeof(UserRootFolder),
- typeof(UserView),
- typeof(Video),
- typeof(Year),
- typeof(Channel),
- typeof(AggregateFolder)
- };
-
public void UpdateInheritedValues()
{
string sql = string.Join(
@@ -4893,9 +4890,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict;
}
- // Not crazy about having this all the way down here, but at least it's in one place
- private readonly Dictionary _types = GetTypeMapDictionary();
-
private string MapIncludeItemTypes(string value)
{
if (_types.TryGetValue(value, out string result))
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 829f1de2f6..107096b5f2 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
///
/// Opens the connection to the database.
///
+ /// The user manager.
+ /// The lock to use for database IO.
+ /// The connection to use for database IO.
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{
WriteLock.Dispose();
@@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- db.ExecuteAll(string.Join(';', new[] {
-
+ db.ExecuteAll(string.Join(';', new[]
+ {
"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",
diff --git a/Emby.Server.Implementations/Data/SynchronouseMode.cs b/Emby.Server.Implementations/Data/SynchronouseMode.cs
new file mode 100644
index 0000000000..cde524e2e0
--- /dev/null
+++ b/Emby.Server.Implementations/Data/SynchronouseMode.cs
@@ -0,0 +1,30 @@
+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
new file mode 100644
index 0000000000..d2427ce478
--- /dev/null
+++ b/Emby.Server.Implementations/Data/TempStoreMode.cs
@@ -0,0 +1,23 @@
+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 3d15b3e768..0cfced8beb 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger _logger;
-
private readonly object _syncLock = new object();
+ private string _id;
+
+ public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
+ {
+ _appPaths = appPaths;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public string Value => _id ?? (_id = GetDeviceId());
+
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId()
@@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
return id;
}
-
- private string _id;
-
- public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
- {
- _appPaths = appPaths;
- _logger = loggerFactory.CreateLogger();
- }
-
- public string Value => _id ?? (_id = GetDeviceId());
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 74400b5128..9287f52728 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -420,7 +420,7 @@ namespace Emby.Server.Implementations.Dto
// Just return something so that apps that are expecting a value won't think the folders are empty
if (folder is ICollectionFolder || folder is UserView)
{
- return new Random().Next(1, 10);
+ return Random.Shared.Next(1, 10);
}
return folder.GetChildCount(user);
@@ -755,15 +755,6 @@ namespace Emby.Server.Implementations.Dto
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
}
- if (options.ContainsField(ItemFields.ScreenshotImageTags))
- {
- var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
- if (screenshotLimit > 0)
- {
- dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
- }
- }
-
if (options.ContainsField(ItemFields.Genres))
{
dto.Genres = item.Genres;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index fa24e9dd1d..c1ce4b5572 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,18 +23,18 @@
-
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
+
@@ -42,16 +42,11 @@
- net5.0
+ net6.0
false
true
AD0001
- false
-
-
-
- true
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 0a4efd73c7..640754af40 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -9,7 +9,6 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index df48346e3c..331de45c1e 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
///
/// Translates the physical item to user library.
///
- ///
+ /// The type of item.
/// The item.
/// The user.
/// if set to true [include if not found].
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7010a6fb08..5f25f69801 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
///
/// Sends a message asynchronously.
///
- ///
+ /// The type of the message.
/// The message.
/// The cancellation token.
/// Task.
@@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
- } while (
- (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ }
+ while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index aa80bccd72..e9d069cd33 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -41,6 +41,25 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The library manager.
+ /// The configuration manager.
+ /// The filesystem.
+ public LibraryMonitor(
+ ILogger logger,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _configurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ }
+
///
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
///
@@ -95,21 +114,6 @@ namespace Emby.Server.Implementations.IO
}
}
- ///
- /// Initializes a new instance of the class.
- ///
- public LibraryMonitor(
- ILogger logger,
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- }
-
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
@@ -199,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// The LST.
/// The path.
/// true if [contains parent folder] [the specified LST]; otherwise, false.
- /// path
+ /// is null.
private static bool ContainsParentFolder(IEnumerable lst, string path)
{
if (string.IsNullOrEmpty(path))
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 2c722ff25a..eeee288425 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -5,11 +5,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@@ -19,7 +17,7 @@ namespace Emby.Server.Implementations.IO
///
public class ManagedFileSystem : IFileSystem
{
- protected ILogger Logger;
+ private readonly ILogger _logger;
private readonly List _shortcutHandlers = new List();
private readonly string _tempPath;
@@ -29,7 +27,7 @@ namespace Emby.Server.Implementations.IO
ILogger logger,
IApplicationPaths applicationPaths)
{
- Logger = logger;
+ _logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.IO
///
/// The filename.
/// true if the specified filename is shortcut; otherwise, false.
- /// filename
+ /// is null.
public virtual bool IsShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -60,7 +58,7 @@ namespace Emby.Server.Implementations.IO
///
/// The filename.
/// System.String.
- /// filename
+ /// is null.
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -235,9 +233,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
- //{
+ // {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- //}
+ // }
if (info is FileInfo fileInfo)
{
@@ -248,15 +246,15 @@ namespace Emby.Server.Implementations.IO
{
try
{
- using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
- result.Length = thisFileStream.Length;
+ result.Length = RandomAccess.GetLength(fileHandle);
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
- Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
+ _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -345,7 +343,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -384,7 +382,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 1d97882db5..3769ae4dd8 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Server.Implementations
{
+ ///
+ /// Specifies the contract for server startup options.
+ ///
public interface IStartupOptions
{
///
@@ -10,7 +11,7 @@ namespace Emby.Server.Implementations
string? FFmpegPath { get; }
///
- /// Gets a value value indicating whether to run as service by the --service command line option.
+ /// Gets a value indicating whether to run as service by the --service command line option.
///
bool IsService { get; }
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 4a026fd218..7589869457 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -65,13 +65,13 @@ namespace Emby.Server.Implementations.Images
if (SupportedImages.Contains(ImageType.Primary))
{
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | primaryResult;
+ updateType |= primaryResult;
}
if (SupportedImages.Contains(ImageType.Thumb))
{
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | thumbResult;
+ updateType |= thumbResult;
}
return updateType;
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
new file mode 100644
index 0000000000..1c69056d20
--- /dev/null
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -0,0 +1,67 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ public abstract class BaseFolderImageProvider : BaseDynamicImageProvider
+ where T : Folder, new()
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ protected override IReadOnlyList GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Parent = item,
+ DtoOptions = new DtoOptions(true),
+ ImageTypes = new ImageType[] { ImageType.Primary },
+ OrderBy = new (string, SortOrder)[]
+ {
+ (ItemSortBy.IsFolder, SortOrder.Ascending),
+ (ItemSortBy.SortName, SortOrder.Ascending)
+ },
+ Limit = 1
+ });
+ }
+
+ protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
+ {
+ return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
+ }
+
+ protected override bool Supports(BaseItem item)
+ {
+ return item is T;
+ }
+
+ protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
+ {
+ if (item is MusicAlbum)
+ {
+ return false;
+ }
+
+ return base.HasChangedByDate(item, image);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 859017f869..4376bd356c 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -2,69 +2,16 @@
#pragma warning disable CS1591
-using System.Collections.Generic;
-using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
- public abstract class BaseFolderImageProvider : BaseDynamicImageProvider
- where T : Folder, new()
- {
- protected ILibraryManager _libraryManager;
-
- public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- protected override IReadOnlyList GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Parent = item,
- DtoOptions = new DtoOptions(true),
- ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new System.ValueTuple[]
- {
- new System.ValueTuple(ItemSortBy.IsFolder, SortOrder.Ascending),
- new System.ValueTuple(ItemSortBy.SortName, SortOrder.Ascending)
- },
- Limit = 1
- });
- }
-
- protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
- {
- return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
- }
-
- protected override bool Supports(BaseItem item)
- {
- return item is T;
- }
-
- protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
- {
- if (item is MusicAlbum)
- {
- return false;
- }
-
- return base.HasChangedByDate(item, image);
- }
- }
-
public class FolderImageProvider : BaseFolderImageProvider
{
public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
@@ -87,20 +34,4 @@ namespace Emby.Server.Implementations.Images
return true;
}
}
-
- public class MusicAlbumImageProvider : BaseFolderImageProvider
- {
- public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
-
- public class PhotoAlbumImageProvider : BaseFolderImageProvider
- {
- public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
}
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 6da431c68e..1f5090f7f5 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -19,46 +18,6 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
- ///
- /// Class MusicGenreImageProvider.
- ///
- public class MusicGenreImageProvider : BaseDynamicImageProvider
- {
- ///
- /// The library manager.
- ///
- private readonly ILibraryManager _libraryManager;
-
- public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- ///
- /// Get children objects used to create an music genre image.
- ///
- /// The music genre used to create the image.
- /// Any relevant children objects.
- protected override IReadOnlyList GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Genres = new[] { item.Name },
- IncludeItemTypes = new[]
- {
- nameof(MusicAlbum),
- nameof(MusicVideo),
- nameof(Audio)
- },
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
- Limit = 4,
- Recursive = true,
- ImageTypes = new[] { ImageType.Primary },
- DtoOptions = new DtoOptions(false)
- });
- }
- }
-
///
/// Class GenreImageProvider.
///
diff --git a/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
new file mode 100644
index 0000000000..ce83673638
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class MusicAlbumImageProvider : BaseFolderImageProvider
+ {
+ public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
new file mode 100644
index 0000000000..baf1c90517
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
@@ -0,0 +1,59 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ ///
+ /// Class MusicGenreImageProvider.
+ ///
+ public class MusicGenreImageProvider : BaseDynamicImageProvider
+ {
+ ///
+ /// The library manager.
+ ///
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ ///
+ /// Get children objects used to create an music genre image.
+ ///
+ /// The music genre used to create the image.
+ /// Any relevant children objects.
+ protected override IReadOnlyList GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Genres = new[] { item.Name },
+ IncludeItemTypes = new[]
+ {
+ nameof(MusicAlbum),
+ nameof(MusicVideo),
+ nameof(Audio)
+ },
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
+ Limit = 4,
+ Recursive = true,
+ ImageTypes = new[] { ImageType.Primary },
+ DtoOptions = new DtoOptions(false)
+ });
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
new file mode 100644
index 0000000000..1ddb4c7579
--- /dev/null
+++ b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class PhotoAlbumImageProvider : BaseFolderImageProvider
+ {
+ public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index c7d1139639..bc5b4499fc 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Ignore trailer folders but allow it at the collection level
- if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
+ if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
&& !(parent is AggregateFolder)
&& !(parent is UserRootFolder))
{
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 6c65b58999..868071a992 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
@@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn();
}
+ public Stream GetStream()
+ {
+ throw new NotSupportedException();
+ }
+
public Task Open(CancellationToken openCancellationToken)
{
return Task.CompletedTask;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 8054beae3f..1326f60fe5 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.Library
/// Determines whether a path should be ignored based on its contents - called after the contents have been read.
///
/// The args.
- /// true if XXXX, false otherwise
+ /// true if XXXX, false otherwise.
private static bool ShouldResolvePathContents(ItemResolveArgs args)
{
// Ignore any folders containing a file called .ignore
@@ -1250,10 +1250,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
- foreach (var file in files)
+ foreach (ReadOnlySpan file in files)
{
- // TODO: @bond use a ReadOnlySpan here when Enum.TryParse supports it
- // https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
@@ -1268,7 +1266,7 @@ namespace Emby.Server.Implementations.Library
///
/// The id.
/// BaseItem.
- /// id
+ /// is null.
public BaseItem GetItemById(Guid id)
{
if (id == Guid.Empty)
@@ -2714,7 +2712,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
@@ -2758,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 8062691827..83acd8e9f2 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -10,13 +10,14 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.OpenWrite(cacheFilePath);
+ await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 91c9e61cf3..351fced348 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -13,9 +13,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -521,7 +521,7 @@ namespace Emby.Server.Implementations.Library
// TODO: @bond Fix
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
- _logger.LogInformation("Live stream opened: " + json);
+ _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
@@ -587,13 +587,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
- {
- var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
-
- return Task.FromResult(info.Value as IDirectStreamProvider);
- }
-
public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@@ -602,7 +595,8 @@ namespace Emby.Server.Implementations.Library
public async Task GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{
- var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
var mediaSource = liveStreamInfo.MediaSource;
@@ -638,7 +632,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -771,18 +765,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true);
}
- public async Task> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ public Task> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
- return new Tuple(info.MediaSource, info as IDirectStreamProvider);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
+ return Task.FromResult(new Tuple(info.MediaSource, info as IDirectStreamProvider));
}
- private Task GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ public ILiveStream GetLiveStreamInfo(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -791,12 +786,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- return Task.FromResult(info);
- }
- else
- {
- return Task.FromException(new ResourceNotFoundException());
+ return info;
}
+
+ return null;
+ }
+
+ ///
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
+ {
+ return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
}
public async Task GetLiveStream(string id, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index b833122ea0..b3837fedb4 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -38,14 +38,11 @@ namespace Emby.Server.Implementations.Library
}
public static int? GetDefaultSubtitleStreamIndex(
- List streams,
+ IEnumerable streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
{
- streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
- .ToList();
-
MediaStream stream = null;
if (mode == SubtitlePlaybackMode.None)
@@ -53,52 +50,48 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ var sortedStreams = streams
+ .Where(i => i.Type == MediaStreamType.Subtitle)
+ .OrderByDescending(x => x.IsExternal)
+ .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(x => x.IsForced)
+ .ThenByDescending(x => x.IsDefault)
+ .ToList();
+
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
-
- stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => s.IsForced) ??
- streams.FirstOrDefault(s => s.IsDefault);
+ stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
// if the audio language is not understood by the user, load their preferred subs, if there are any
if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
{
- stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
}
}
else if (mode == SubtitlePlaybackMode.Smart)
{
- // Prefer smart logic over embedded metadata
-
// if the audio language is not understood by the user, load their preferred subs, if there are any
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
{
- stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
+ stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
// always load the most suitable full subtitles
- stream = streams.FirstOrDefault(s => !s.IsForced);
+ stream = sortedStreams.FirstOrDefault(s => !s.IsForced);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// always load the most suitable full subtitles
- stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => s.IsForced);
+ stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
// load forced subs if we have found no suitable full subtitles
- stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
-
- if (stream != null)
- {
- return stream.Index;
- }
-
- return null;
+ stream ??= sortedStreams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+ return stream?.Index;
}
private static IEnumerable GetSortedStreams(IEnumerable streams, MediaStreamType type, string[] languagePreferences)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 86b8039fab..d5b855cdf2 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library
/// The original path.
/// The original sub path.
/// The new sub path.
- /// The result of the sub path replacement
+ /// The result of the sub path replacement.
/// The path after replacing the sub path.
/// , or is empty.
public static bool TryReplaceSubPath(
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 8e1eccb10a..60720dd2f7 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -82,6 +82,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
///
/// Determine if the supplied file data points to a music album.
///
+ /// The path to check.
+ /// The directory service.
+ /// true if the provided path points to a music album, false otherwise.
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index b102b86cfb..9ff99fa431 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -275,6 +275,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
///
/// Determines whether [is DVD directory] [the specified directory name].
///
+ /// The full path of the directory.
+ /// The name of the directory.
+ /// The directory service.
+ /// true if the provided directory is a DVD directory, false otherwise.
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
{
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 68076730b3..e685c87f1a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -49,13 +49,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
- var fileExtension = Path.GetExtension(f.FullName) ??
- string.Empty;
+ var fileExtension = Path.GetExtension(f.FullName)
+ ?? string.Empty;
return _validExtensions.Contains(
fileExtension,
- StringComparer
- .OrdinalIgnoreCase);
+ StringComparer.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7aaee017dd..db7703cd60 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
///
/// Class FolderResolver.
///
- public class FolderResolver : FolderResolver
+ public class FolderResolver : GenericFolderResolver
{
///
/// Gets the priority.
@@ -32,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
}
-
- ///
- /// Class FolderResolver.
- ///
- /// The type of the T item type.
- public abstract class FolderResolver : ItemResolver
- where TItemType : Folder, new()
- {
- ///
- /// Sets the initial item values.
- ///
- /// The item.
- /// The args.
- protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- item.IsRoot = args.Parent == null;
- }
- }
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
new file mode 100644
index 0000000000..f109a5e9a2
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
@@ -0,0 +1,27 @@
+#nullable disable
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ ///
+ /// Class FolderResolver.
+ ///
+ /// The type of the T item type.
+ public abstract class GenericFolderResolver : ItemResolver
+ where TItemType : Folder, new()
+ {
+ ///
+ /// Sets the initial item values.
+ ///
+ /// The item.
+ /// The args.
+ protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 69d71d0d9a..e7abe1e6da 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
///
/// Class BoxSetResolver.
///
- public class BoxSetResolver : FolderResolver
+ public class BoxSetResolver : GenericFolderResolver
{
///
/// Resolves the specified args.
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8b55a77449..f3b6ef0a28 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -24,6 +24,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
///
public class MovieResolver : BaseVideoResolver
public class UserDataManager : IUserDataManager
{
- public event EventHandler UserDataSaved;
-
private readonly ConcurrentDictionary _userData =
new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
@@ -44,6 +42,8 @@ namespace Emby.Server.Implementations.Library
_repository = repository;
}
+ public event EventHandler UserDataSaved;
+
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -90,10 +90,9 @@ namespace Emby.Server.Implementations.Library
///
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
///
- ///
- ///
- ///
- ///
+ /// The user id.
+ /// The user item data.
+ /// The cancellation token.
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -104,8 +103,8 @@ namespace Emby.Server.Implementations.Library
///
/// Retrieve all user data for the given user.
///
- ///
- ///
+ /// The user id.
+ /// A containing all of the user's item data.
public List GetAllUserData(Guid userId)
{
var user = _userManager.GetUserById(userId);
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e2da672a3c..711647e7a6 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -341,19 +341,26 @@ namespace Emby.Server.Implementations.Library
mediaTypes = mediaTypes.Distinct().ToList();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
- {
- nameof(Person),
- nameof(Studio),
- nameof(Year),
- nameof(MusicGenre),
- nameof(Genre)
- } : Array.Empty();
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ ? new[]
+ {
+ nameof(Person),
+ nameof(Studio),
+ nameof(Year),
+ nameof(MusicGenre),
+ nameof(Genre)
+ }
+ : Array.Empty();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = new[]
+ {
+ (ItemSortBy.DateCreated, SortOrder.Descending),
+ (ItemSortBy.SortName, SortOrder.Descending),
+ (ItemSortBy.ProductionYear, SortOrder.Descending)
+ },
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index f9a3e2c64b..40436d9c5d 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -95,10 +95,13 @@ namespace Emby.Server.Implementations.Library.Validators
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- }, false);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
progress.Report(100);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index bb3d635d12..6937cc0971 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -45,21 +46,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
onStarted();
- _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+ _logger.LogInformation("Copying recording to file {FilePath}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+ var linkedCancellationToken = cancellationTokenSource.Token;
- await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
+ await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
+ await _streamHelper.CopyToAsync(
+ fileStream,
+ output,
+ IODefaults.CopyToBufferSize,
+ 1000,
+ linkedCancellationToken).ConfigureAwait(false);
}
- _logger.LogInformation("Recording completed to file {0}", targetFile);
+ _logger.LogInformation("Recording completed: {FilePath}", targetFile);
}
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
@@ -71,8 +78,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
+ await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 67f824e71d..8045e814c2 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1848,14 +1848,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
using (var writer = XmlWriter.Create(stream, settings))
@@ -1913,14 +1911,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
var options = _config.GetNfoConfiguration();
@@ -1990,7 +1986,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString(
"dateadded",
- DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
+ DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index e10bc76470..835028b92a 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
@@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
- targetFile,
+ targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
@@ -205,9 +205,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// var audioChannels = 2;
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
// if (audioStream != null)
- //{
+ // {
// audioChannels = audioStream.Channels ?? audioChannels;
- //}
+ // }
// return "-codec:a:0 aac -strict experimental -ab 320000";
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index dfe3517b22..7705132da2 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -13,6 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
///
/// Records the specified media source.
///
+ /// The direct stream provider, or null.
+ /// The media source.
+ /// The target file.
+ /// The duration to record.
+ /// An action to perform when recording starts.
+ /// The cancellation token.
+ /// A that represents the recording operation.
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 4a031e4752..46979bfc57 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,9 +1,8 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private T[] _items;
+ private T[]? _items;
public ItemDataProvider(
ILogger logger,
@@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
protected Func EqualityComparer { get; }
+ [MemberNotNull(nameof(_items))]
private void EnsureLoaded()
{
if (_items != null)
@@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize(bytes, _jsonOptions);
+ if (_items == null)
+ {
+ Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
+ _items = Array.Empty();
+ }
+
return;
}
catch (JsonException ex)
@@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
- Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString);
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 8125ed57df..1f963e4a29 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,14 +9,15 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
@@ -34,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
- private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary();
@@ -44,12 +44,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public SchedulesDirect(
ILogger logger,
IHttpClientFactory httpClientFactory,
- IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
_cryptoProvider = cryptoProvider;
}
@@ -114,18 +112,29 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (dailySchedules == null)
+ {
+ return Array.Empty();
+ }
+
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
- var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
- programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
+ var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
+ programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
+ programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (programDetails == null)
+ {
+ return Array.Empty();
+ }
+
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
@@ -142,6 +151,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
// schedule.ProgramId + " which says it has images? " +
// programDict[schedule.ProgramId].hasImageArtwork);
+ if (string.IsNullOrEmpty(schedule.ProgramId))
+ {
+ continue;
+ }
+
if (images != null)
{
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
@@ -149,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var programEntry = programDict[schedule.ProgramId];
- var allImages = images[imageIndex].Data ?? new List();
+ var allImages = images[imageIndex].Data;
var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
@@ -217,7 +231,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
{
- var startAt = GetDate(programInfo.AirDateTime);
+ if (programInfo.AirDateTime == null)
+ {
+ return null;
+ }
+
+ var startAt = programInfo.AirDateTime.Value;
var endAt = startAt.AddSeconds(programInfo.Duration);
var audioType = ProgramAudio.Stereo;
@@ -225,21 +244,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
- if (programInfo.AudioProperties != null)
+ if (programInfo.AudioProperties.Count != 0)
{
- if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
+ if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Atmos;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Stereo;
}
@@ -355,9 +374,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- if (!string.IsNullOrWhiteSpace(details.OriginalAirDate))
+ if (details.OriginalAirDate != null)
{
- info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture);
+ info.OriginalAirDate = details.OriginalAirDate;
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
@@ -384,18 +403,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private static DateTime GetDate(string value)
- {
- var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture);
-
- if (date.Kind != DateTimeKind.Utc)
- {
- date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
- }
-
- return date;
- }
-
private string GetProgramImage(string apiUrl, IEnumerable images, bool returnDefaultImage, double desiredAspect)
{
var match = images
@@ -449,14 +456,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return result;
}
- private async Task> GetImageForPrograms(
+ private async Task> GetImageForPrograms(
ListingsProviderInfo info,
IReadOnlyList programIds,
CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
- return new List();
+ return Array.Empty();
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -480,13 +487,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return new List();
+ return Array.Empty();
}
}
@@ -509,7 +516,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
@@ -520,7 +527,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
lineups.Add(new NameIdPair
{
Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
- Id = lineup.Uri[18..]
+ Id = lineup.Uri?[18..]
});
}
}
@@ -651,7 +658,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(root.Message, "OK", StringComparison.Ordinal))
+ if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
return root.Token;
@@ -708,12 +715,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = httpResponse.Content;
var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase));
+ return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
{
// SchedulesDirect returns 400 if no lineups are configured.
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ if (ex.StatusCode is HttpStatusCode.BadRequest)
{
return false;
}
@@ -779,10 +786,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return new List();
+ }
+
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
_logger.LogInformation("Mapping Stations to Channel");
- var allStations = root.Stations ?? new List();
+ var allStations = root.Stations;
var map = root.Map;
var list = new List(map.Count);
@@ -790,11 +802,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase))
- ?? new StationDto
- {
- StationId = channel.StationId
- };
+ var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase));
+ var station = stationIndex == -1
+ ? new StationDto { StationId = channel.StationId }
+ : allStations[stationIndex];
var channelInfo = new ChannelInfo
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
index b881b307c3..95ac996e01 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,24 +11,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the city.
///
[JsonPropertyName("city")]
- public string City { get; set; }
+ public string? City { get; set; }
///
/// Gets or sets the state.
///
[JsonPropertyName("state")]
- public string State { get; set; }
+ public string? State { get; set; }
///
/// Gets or sets the postal code.
///
[JsonPropertyName("postalCode")]
- public string Postalcode { get; set; }
+ public string? Postalcode { get; set; }
///
/// Gets or sets the country.
///
[JsonPropertyName("country")]
- public string Country { get; set; }
+ public string? Country { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
index 96b67d1eb3..f6251b9ad8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the content.
///
[JsonPropertyName("content")]
- public string Content { get; set; }
+ public string? Content { get; set; }
///
/// Gets or sets the lang.
///
[JsonPropertyName("lang")]
- public string Lang { get; set; }
+ public string? Lang { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
index dac6f5f3e0..0b7a2c63ad 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,36 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the billing order.
///
[JsonPropertyName("billingOrder")]
- public string BillingOrder { get; set; }
+ public string? BillingOrder { get; set; }
///
/// Gets or sets the role.
///
[JsonPropertyName("role")]
- public string Role { get; set; }
+ public string? Role { get; set; }
///
/// Gets or sets the name id.
///
[JsonPropertyName("nameId")]
- public string NameId { get; set; }
+ public string? NameId { get; set; }
///
/// Gets or sets the person id.
///
[JsonPropertyName("personId")]
- public string PersonId { get; set; }
+ public string? PersonId { get; set; }
///
/// Gets or sets the name.
///
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
///
/// Gets or sets the character name.
///
[JsonPropertyName("characterName")]
- public string CharacterName { get; set; }
+ public string? CharacterName { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
index 8c9c2c1fcc..87c327ed82 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,18 +13,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the list of maps.
///
[JsonPropertyName("map")]
- public List Map { get; set; }
+ public IReadOnlyList Map { get; set; } = Array.Empty();
///
/// Gets or sets the list of stations.
///
[JsonPropertyName("stations")]
- public List Stations { get; set; }
+ public IReadOnlyList Stations { get; set; } = Array.Empty();
///
/// Gets or sets the metadata.
///
[JsonPropertyName("metadata")]
- public MetadataDto Metadata { get; set; }
+ public MetadataDto? Metadata { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
index 135b5bb08b..c19cd2e48d 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the body.
///
[JsonPropertyName("body")]
- public string Body { get; set; }
+ public string? Body { get; set; }
///
/// Gets or sets the code.
///
[JsonPropertyName("code")]
- public string Code { get; set; }
+ public string? Code { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
index 82d1001c88..f00c9accdd 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,30 +11,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the billing order.
///
[JsonPropertyName("billingOrder")]
- public string BillingOrder { get; set; }
+ public string? BillingOrder { get; set; }
///
/// Gets or sets the role.
///
[JsonPropertyName("role")]
- public string Role { get; set; }
+ public string? Role { get; set; }
///
/// Gets or sets the name id.
///
[JsonPropertyName("nameId")]
- public string NameId { get; set; }
+ public string? NameId { get; set; }
///
/// Gets or sets the person id.
///
[JsonPropertyName("personId")]
- public string PersonId { get; set; }
+ public string? PersonId { get; set; }
///
/// Gets or sets the name.
///
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
index 68876b0686..1a371965cf 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -10,30 +9,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
///
public class DayDto
{
- ///
- /// Initializes a new instance of the class.
- ///
- public DayDto()
- {
- Programs = new List();
- }
-
///
/// Gets or sets the station id.
///
[JsonPropertyName("stationID")]
- public string StationId { get; set; }
+ public string? StationId { get; set; }
///
/// Gets or sets the list of programs.
///
[JsonPropertyName("programs")]
- public List Programs { get; set; }
+ public IReadOnlyList Programs { get; set; } = Array.Empty();
///
/// Gets or sets the metadata schedule.
///
[JsonPropertyName("metadata")]
- public MetadataScheduleDto Metadata { get; set; }
+ public MetadataScheduleDto? Metadata { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
index d3e6ff3935..ca6ae7fb13 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the description language.
///
[JsonPropertyName("descriptionLanguage")]
- public string DescriptionLanguage { get; set; }
+ public string? DescriptionLanguage { get; set; }
///
/// Gets or sets the description.
///
[JsonPropertyName("description")]
- public string Description { get; set; }
+ public string? Description { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
index 04360266c9..1577219ed2 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the description language.
///
[JsonPropertyName("descriptionLanguage")]
- public string DescriptionLanguage { get; set; }
+ public string? DescriptionLanguage { get; set; }
///
/// Gets or sets the description.
///
[JsonPropertyName("description")]
- public string Description { get; set; }
+ public string? Description { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
index 3af36ae965..eaf4a340bd 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the list of description 100.
///
[JsonPropertyName("description100")]
- public List Description100 { get; set; }
+ public IReadOnlyList Description100 { get; set; } = Array.Empty();
///
/// Gets or sets the list of description1000.
///
[JsonPropertyName("description1000")]
- public List Description1000 { get; set; }
+ public IReadOnlyList Description1000 { get; set; } = Array.Empty();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
index c3b2bd9c13..fbdfb1f716 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,6 +11,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the sub type.
///
[JsonPropertyName("subType")]
- public string SubType { get; set; }
+ public string? SubType { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
index 3d8bea362a..6852d89d71 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
index 1fb3decb23..b9844562f3 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,24 +13,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the headend.
///
[JsonPropertyName("headend")]
- public string Headend { get; set; }
+ public string? Headend { get; set; }
///
/// Gets or sets the transport.
///
[JsonPropertyName("transport")]
- public string Transport { get; set; }
+ public string? Transport { get; set; }
///
/// Gets or sets the location.
///
[JsonPropertyName("location")]
- public string Location { get; set; }
+ public string? Location { get; set; }
///
/// Gets or sets the list of lineups.
///
[JsonPropertyName("lineups")]
- public List Lineups { get; set; }
+ public IReadOnlyList Lineups { get; set; } = Array.Empty();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
index 912e680dd7..a1ae3ca6d4 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,60 +11,60 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the width.
///
[JsonPropertyName("width")]
- public string Width { get; set; }
+ public string? Width { get; set; }
///
/// Gets or sets the height.
///
[JsonPropertyName("height")]
- public string Height { get; set; }
+ public string? Height { get; set; }
///
/// Gets or sets the uri.
///
[JsonPropertyName("uri")]
- public string Uri { get; set; }
+ public string? Uri { get; set; }
///
/// Gets or sets the size.
///
[JsonPropertyName("size")]
- public string Size { get; set; }
+ public string? Size { get; set; }
///
/// Gets or sets the aspect.
///
[JsonPropertyName("aspect")]
- public string aspect { get; set; }
+ public string? Aspect { get; set; }
///
/// Gets or sets the category.
///
[JsonPropertyName("category")]
- public string Category { get; set; }
+ public string? Category { get; set; }
///
/// Gets or sets the text.
///
[JsonPropertyName("text")]
- public string Text { get; set; }
+ public string? Text { get; set; }
///
/// Gets or sets the primary.
///
[JsonPropertyName("primary")]
- public string Primary { get; set; }
+ public string? Primary { get; set; }
///
/// Gets or sets the tier.
///
[JsonPropertyName("tier")]
- public string Tier { get; set; }
+ public string? Tier { get; set; }
///
/// Gets or sets the caption.
///
[JsonPropertyName("caption")]
- public CaptionDto Caption { get; set; }
+ public CaptionDto? Caption { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
index 52e920aa61..3dc64e5d8a 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,30 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the linup.
///
[JsonPropertyName("lineup")]
- public string Lineup { get; set; }
+ public string? Lineup { get; set; }
///
/// Gets or sets the lineup name.
///
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
///
/// Gets or sets the transport.
///
[JsonPropertyName("transport")]
- public string Transport { get; set; }
+ public string? Transport { get; set; }
///
/// Gets or sets the location.
///
[JsonPropertyName("location")]
- public string Location { get; set; }
+ public string? Location { get; set; }
///
/// Gets or sets the uri.
///
[JsonPropertyName("uri")]
- public string Uri { get; set; }
+ public string? Uri { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this lineup was deleted.
+ ///
+ [JsonPropertyName("isDeleted")]
+ public bool? IsDeleted { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
index 15139ba3b2..f190817813 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -20,18 +19,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the server id.
///
[JsonPropertyName("serverID")]
- public string ServerId { get; set; }
+ public string? ServerId { get; set; }
///
/// Gets or sets the datetime.
///
[JsonPropertyName("datetime")]
- public string Datetime { get; set; }
+ public DateTime? LineupTimestamp { get; set; }
///
/// Gets or sets the list of lineups.
///
[JsonPropertyName("lineups")]
- public List Lineups { get; set; }
+ public IReadOnlyList Lineups { get; set; } = Array.Empty();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
index 7b235ed7f6..fecc55e037 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,7 +11,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the url.
///
[JsonPropertyName("URL")]
- public string Url { get; set; }
+ public string? Url { get; set; }
///