diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 31f861f63f..cf74a4201b 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -34,7 +34,6 @@ 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 1086d51d27..b7112ba245 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -54,7 +54,6 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
- includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Publish Server'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index adbb056ec8..19d65ea0c1 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -39,6 +39,10 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
@@ -80,6 +84,10 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
@@ -181,7 +189,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
@@ -199,7 +207,6 @@ jobs:
inputs:
packageType: 'sdk'
version: '6.0.x'
- includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -212,6 +219,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)
@@ -226,6 +234,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 80a5732eee..cc94dc2c5a 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -41,7 +41,6 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
- includePreviewVersions: true
- task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud'
diff --git a/.copr b/.copr
new file mode 120000
index 0000000000..100fe0cd7b
--- /dev/null
+++ b/.copr
@@ -0,0 +1 @@
+fedora
\ No newline at end of file
diff --git a/.copr/Makefile b/.copr/Makefile
deleted file mode 120000
index ec3c90dfd9..0000000000
--- a/.copr/Makefile
+++ /dev/null
@@ -1 +0,0 @@
-../fedora/Makefile
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index c1d49778e3..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-name: Bug report
-about: Create a bug report
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-
-
-**System (please complete the following information):**
- - OS: [e.g. Debian, Windows]
- - Virtualization: [e.g. Docker, KVM, LXC]
- - Clients: [Browser, Android, Fire Stick, etc.]
- - Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
- - Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
- - FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- - Playback: [Direct Play, Remux, Direct Stream, Transcode]
- - Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- - Reverse Proxy: [e.g. none, nginx, apache, etc.]
- - Base URL: [e.g. none, yes: /example]
- - Networking: [e.g. Host, Bridge/NAT]
- - Storage: [e.g. local, NFS, cloud]
-
-**To Reproduce**
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-
-
-**Server Logs**
-
-
-**FFmpeg Logs**
-
-
-**Browser Console Logs**
-
-
-**Screenshots**
-
-
-**Additional context**
-
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
new file mode 100644
index 0000000000..63e0f0e22d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -0,0 +1,106 @@
+name: Issue Report
+description: File an issue report
+title: "[Issue]: "
+labels: [bug, triage]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: Please describe your bug
+ description: Also tell us, what did you expect to happen?
+ placeholder: |
+ The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
+
+ This is my issue.
+
+ Steps to Reproduce
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
+ validations:
+ required: true
+ - type: dropdown
+ id: version
+ attributes:
+ label: Jellyfin Version
+ description: What version of Jellyfin are you running?
+ options:
+ - 10.7.7
+ - 10.7.z
+ - 10.6.4
+ - Other
+ validations:
+ required: true
+ - type: input
+ id: version-other
+ attributes:
+ label: "if other:"
+ placeholder: Other
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ Examples:
+ - **OS**: [e.g. Debian, Windows]
+ - **Virtualization**: [e.g. Docker, KVM, LXC]
+ - **Clients**: [Browser, Android, Fire Stick, etc.]
+ - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
+ - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+ - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
+ - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+ - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
+ - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
+ - **Base URL**: [e.g. none, yes: /example]
+ - **Networking**: [e.g. Host, Bridge/NAT]
+ - **Storage**: [e.g. local, NFS, cloud]
+ value: |
+ - OS:
+ - Virtualization:
+ - Clients:
+ - Browser:
+ - FFmpeg Version:
+ - Playback Method:
+ - Hardware Acceleration:
+ - Plugins:
+ - Reverse Proxy:
+ - Base URL:
+ - Networking:
+ - Storage:
+ render: markdown
+ - type: textarea
+ id: logs
+ attributes:
+ label: Jellyfin logs
+ description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+ placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
+ render: shell
+ - type: textarea
+ id: ffmpeg-logs
+ attributes:
+ label: FFmpeg logs
+ description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+ placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+ render: shell
+ - type: textarea
+ id: browserlogs
+ attributes:
+ label: Please attach any browser or client logs here
+ placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Please attach any screenshots here
+ placeholder: Images can be pasted directly into the textbox and will be hosted by github.
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index e07d913b5a..ea1d30cdfa 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -25,8 +25,7 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- include-prerelease: true
-
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
new file mode 100644
index 0000000000..3e93468401
--- /dev/null
+++ b/.github/workflows/openapi.yml
@@ -0,0 +1,124 @@
+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'
+ - 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'
+ - 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..d52e133249 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -149,6 +149,8 @@
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
+ - [jonas-resch](https://github.com/jonas-resch)
# Emby Contributors
diff --git a/Directory.Build.props b/Directory.Build.props
index b899999efb..b27782918c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,10 +3,13 @@
enable
- true
$(MSBuildThisFileDirectory)/jellyfin.ruleset
+
+ true
+
+
AllEnabledByDefault
diff --git a/Dockerfile b/Dockerfile
index 73b5908b4e..e133c08193 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,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"
@@ -29,8 +29,9 @@ 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 \
@@ -61,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
@@ -76,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
@@ -85,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 edb8591c64..a46fa331df 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -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 db1edcfe66..1279c47f8e 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -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/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index ac336e5dcc..010f90c624 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -18,23 +18,16 @@ 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.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
@@ -50,7 +43,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataManager;
- private readonly IServerConfigurationManager _config;
private readonly User _user;
private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager;
@@ -104,7 +96,6 @@ namespace Emby.Dlna.ContentDirectory
_userViewManager = userViewManager;
_tvSeriesManager = tvSeriesManager;
_profile = profile;
- _config = config;
_didlBuilder = new DidlBuilder(
profile,
@@ -291,9 +282,9 @@ namespace Emby.Dlna.ContentDirectory
return ""
+ ""
+ ""
- + ""
- + ""
- + ""
+ + ""
+ + ""
+ + ""
+ ""
+ "";
}
@@ -330,75 +321,73 @@ namespace Emby.Dlna.ContentDirectory
int totalCount;
- using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ var settings = new XmlWriterSettings
{
- var settings = new XmlWriterSettings()
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
+
+ using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
+ {
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+
+ var serverItem = GetItemFromObjectId(id);
+ var item = serverItem.Item;
+
+ if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
{
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
+ totalCount = 1;
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(id);
- var item = serverItem.Item;
-
- if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
+ if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{
- totalCount = 1;
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
- {
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
-
- _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
- }
-
- provided++;
+ _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
}
else
{
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- totalCount = childrenResult.TotalRecordCount;
-
- provided = childrenResult.Items.Count;
-
- foreach (var i in childrenResult.Items)
- {
- var childItem = i.Item;
- var displayStubType = i.StubType;
-
- if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
- {
- var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
- .TotalRecordCount;
-
- _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
- }
- }
+ _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
}
- writer.WriteFullEndElement();
+ provided++;
+ }
+ else
+ {
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
+ totalCount = childrenResult.TotalRecordCount;
+
+ provided = childrenResult.Items.Count;
+
+ foreach (var i in childrenResult.Items)
+ {
+ var childItem = i.Item;
+ var displayStubType = i.StubType;
+
+ if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
+ {
+ var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
+ .TotalRecordCount;
+
+ _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
+ }
+ else
+ {
+ _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
+ }
+ }
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -449,53 +438,46 @@ namespace Emby.Dlna.ContentDirectory
}
QueryResult childrenResult;
+ var settings = new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
{
- var settings = new XmlWriterSettings()
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+
+ var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
+
+ var item = serverItem.Item;
+
+ childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
+ foreach (var i in childrenResult.Items)
{
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
-
- var item = serverItem.Item;
-
- childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
-
- var dlnaOptions = _config.GetDlnaConfiguration();
-
- foreach (var i in childrenResult.Items)
+ if (i.IsDisplayedAsFolder)
{
- if (i.IsDisplayedAsFolder)
- {
- var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
- .TotalRecordCount;
+ var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
+ .TotalRecordCount;
- _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
- }
+ _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
+ }
+ else
+ {
+ _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
}
-
- writer.WriteFullEndElement();
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -518,48 +500,38 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
- var sortOrders = folder.IsPreSorted
- ? Array.Empty<(string, SortOrder)>()
- : new[] { (ItemSortBy.SortName, sort.SortOrder) };
-
string[] mediaTypes = Array.Empty();
bool? isFolder = null;
- if (search.SearchType == SearchType.Audio)
+ switch (search.SearchType)
{
- mediaTypes = new[] { MediaType.Audio };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Video)
- {
- mediaTypes = new[] { MediaType.Video };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Image)
- {
- mediaTypes = new[] { MediaType.Photo };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Playlist)
- {
- // items = items.OfType();
- isFolder = true;
- }
- else if (search.SearchType == SearchType.MusicAlbum)
- {
- // items = items.OfType();
- isFolder = true;
+ case SearchType.Audio:
+ mediaTypes = new[] { MediaType.Audio };
+ isFolder = false;
+ break;
+ case SearchType.Video:
+ mediaTypes = new[] { MediaType.Video };
+ isFolder = false;
+ break;
+ case SearchType.Image:
+ mediaTypes = new[] { MediaType.Photo };
+ isFolder = false;
+ break;
+ case SearchType.Playlist:
+ case SearchType.MusicAlbum:
+ isFolder = true;
+ break;
}
return folder.GetItems(new InternalItemsQuery
{
Limit = limit,
StartIndex = startIndex,
- OrderBy = sortOrders,
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted),
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -587,52 +559,49 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
{
- if (item is MusicGenre)
+ switch (item)
{
- return GetMusicGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
+ case MusicGenre:
+ return GetMusicGenreItems(item, user, sort, startIndex, limit);
+ case MusicArtist:
+ return GetMusicArtistItems(item, user, sort, startIndex, limit);
+ case Genre:
+ return GetGenreItems(item, user, sort, startIndex, limit);
}
- if (item is MusicArtist)
+ if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
{
- return GetMusicArtistItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if (item is Genre)
- {
- return GetGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if ((!stubType.HasValue || stubType.Value != StubType.Folder)
- && item is IHasCollectionType collectionFolder)
- {
- if (string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+ var collectionType = collectionFolder.CollectionType;
+ if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Movies, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.TvShows, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetTvFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetFolders(user, startIndex, limit);
}
- else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetLiveTvChannels(user, sort, startIndex, limit);
}
}
- if (stubType.HasValue)
+ if (stubType.HasValue && stubType.Value != StubType.Folder)
{
- if (stubType.Value != StubType.Folder)
- {
- return ApplyPaging(new QueryResult(), startIndex, limit);
- }
+ // TODO should this be doing something?
+ return new QueryResult();
}
var folder = (Folder)item;
@@ -642,13 +611,12 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsPlaceHolder = false,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted)
};
- SetSorting(query, sort, folder.IsPreSorted);
-
var queryResult = folder.GetItems(query);
return ToResult(queryResult);
@@ -668,10 +636,9 @@ namespace Emby.Dlna.ContentDirectory
{
StartIndex = startIndex,
Limit = limit,
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
+ OrderBy = GetOrderBy(sort, false)
};
- query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
-
- SetSorting(query, sort, false);
var result = _libraryManager.GetItemsResult(query);
@@ -693,117 +660,57 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.Latest)
+ switch (stubType)
{
- return GetMusicLatest(item, user, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Audio);
+ case StubType.Playlists:
+ return GetMusicPlaylists(query);
+ case StubType.Albums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
+ case StubType.Artists:
+ return GetMusicArtists(item, query);
+ case StubType.AlbumArtists:
+ return GetMusicAlbumArtists(item, query);
+ case StubType.FavoriteAlbums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
+ case StubType.FavoriteArtists:
+ return GetFavoriteArtists(item, query);
+ case StubType.FavoriteSongs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
+ case StubType.Songs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio);
+ case StubType.Genres:
+ return GetMusicGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Playlists)
+ var serverItems = new ServerItem[]
{
- return GetMusicPlaylists(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Albums)
- {
- return GetMusicAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Artists)
- {
- return GetMusicArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.AlbumArtists)
- {
- return GetMusicAlbumArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteAlbums)
- {
- return GetFavoriteAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteArtists)
- {
- return GetFavoriteArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSongs)
- {
- return GetFavoriteSongs(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Songs)
- {
- return GetMusicSongs(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetMusicGenres(item, user, query);
- }
-
- var list = new List
- {
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Playlists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Albums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.AlbumArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Artists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Songs
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteAlbums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSongs
- }
+ new(item, StubType.Latest),
+ new(item, StubType.Playlists),
+ new(item, StubType.Albums),
+ new(item, StubType.AlbumArtists),
+ new(item, StubType.Artists),
+ new(item, StubType.Songs),
+ new(item, StubType.Genres),
+ new(item, StubType.FavoriteArtists),
+ new(item, StubType.FavoriteAlbums),
+ new(item, StubType.FavoriteSongs)
};
+ if (limit < serverItems.Length)
+ {
+ serverItems = serverItems[..limit.Value];
+ }
+
return new QueryResult
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -822,68 +729,41 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
+ switch (stubType)
{
- return GetMovieContinueWatching(item, user, query);
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Movie);
+ case StubType.Movies:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie);
+ case StubType.Collections:
+ return GetMovieCollections(query);
+ case StubType.Favorites:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Latest)
+ var array = new ServerItem[]
{
- return GetMovieLatest(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Movies)
- {
- return GetMovieMovies(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Collections)
- {
- return GetMovieCollections(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Favorites)
- {
- return GetMovieFavorites(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetGenres(item, user, query);
- }
-
- var array = new[]
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
- new ServerItem(item)
- {
- StubType = StubType.Movies
- },
- new ServerItem(item)
- {
- StubType = StubType.Collections
- },
- new ServerItem(item)
- {
- StubType = StubType.Favorites
- },
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
+ new(item, StubType.ContinueWatching),
+ new(item, StubType.Latest),
+ new(item, StubType.Movies),
+ new(item, StubType.Collections),
+ new(item, StubType.Favorites),
+ new(item, StubType.Genres)
};
+ if (limit < array.Length)
+ {
+ array = array[..limit.Value];
+ }
+
return new QueryResult
{
Items = array,
@@ -900,22 +780,21 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetFolders(User user, int? startIndex, int? limit)
{
- var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true);
+ var totalRecordCount = folders.Count;
+ // Handle paging
+ var items = folders
.OrderBy(i => i.SortName)
- .Select(i => new ServerItem(i)
- {
- StubType = StubType.Folder
- })
+ .Skip(startIndex ?? 0)
+ .Take(limit ?? int.MaxValue)
+ .Select(i => new ServerItem(i, StubType.Folder))
.ToArray();
- return ApplyPaging(
- new QueryResult
- {
- Items = folders,
- TotalRecordCount = folders.Length
- },
- startIndex,
- limit);
+ return new QueryResult
+ {
+ Items = items,
+ TotalRecordCount = totalRecordCount
+ };
}
///
@@ -933,87 +812,48 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
+ switch (stubType)
{
- return GetMovieContinueWatching(item, user, query);
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.NextUp:
+ return GetNextUp(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Episode);
+ case StubType.Series:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series);
+ case StubType.FavoriteSeries:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
+ case StubType.FavoriteEpisodes:
+ return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.NextUp)
+ var serverItems = new ServerItem[]
{
- return GetNextUp(item, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Latest)
- {
- return GetTvLatest(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Series)
- {
- return GetSeries(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSeries)
- {
- return GetFavoriteSeries(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteEpisodes)
- {
- return GetFavoriteEpisodes(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetGenres(item, user, query);
- }
-
- var list = new List
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
-
- new ServerItem(item)
- {
- StubType = StubType.NextUp
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Series
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSeries
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteEpisodes
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
+ new(item, StubType.ContinueWatching),
+ new(item, StubType.NextUp),
+ new(item, StubType.Latest),
+ new(item, StubType.Series),
+ new(item, StubType.FavoriteSeries),
+ new(item, StubType.FavoriteEpisodes),
+ new(item, StubType.Genres)
};
+ if (limit < serverItems.Length)
+ {
+ serverItems = serverItems[..limit.Value];
+ }
+
return new QueryResult
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -1021,14 +861,12 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the Movies that are part watched that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMovieContinueWatching(BaseItem parent, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
query.OrderBy = new[]
{
@@ -1037,47 +875,7 @@ namespace Emby.Dlna.ContentDirectory
};
query.IsResumable = true;
- query.Limit = 10;
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the series meeting the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetSeries(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Series) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the Movie folders meeting the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.Limit ??= 10;
var result = _libraryManager.GetItemsResult(query);
@@ -1087,16 +885,12 @@ namespace Emby.Dlna.ContentDirectory
///
/// Returns the Movie collections meeting the criteria.
///
- /// The see cref="User"/>.
/// The see cref="InternalItemsQuery"/>.
/// The .
- private QueryResult GetMovieCollections(User user, InternalItemsQuery query)
+ private QueryResult GetMovieCollections(InternalItemsQuery query)
{
query.Recursive = true;
- // query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(BoxSet) };
+ query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
var result = _libraryManager.GetItemsResult(query);
@@ -1104,139 +898,19 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Returns the Music albums meeting the criteria.
+ /// Returns the children that meet the criteria.
///
/// The .
- /// The .
/// The .
+ /// The item type.
+ /// A value indicating whether to only fetch favorite items.
/// The .
- private QueryResult GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the Music songs meeting the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Audio) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the songs tagged as favourite that meet the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Audio) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the series tagged as favourite that meet the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Series) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the episodes tagged as favourite that meet the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Episode) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// Returns the movies tagged as favourite that meet the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Movie) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- ///
- /// /// Returns the albums tagged as favourite that meet the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
+ query.IsFavorite = isFavorite;
+ query.IncludeItemTypes = new[] { itemType };
var result = _libraryManager.GetItemsResult(query);
@@ -1248,139 +922,90 @@ namespace Emby.Dlna.ContentDirectory
/// The GetGenres.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetGenres(query);
- var result = new QueryResult
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(genresResult);
}
///
/// Returns the music genres meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetMusicGenres(query);
- var result = new QueryResult
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(genresResult);
}
///
/// Returns the music albums by artist that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetAlbumArtists(query);
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(artists);
}
///
/// Returns the music artists meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
///
/// Returns the artists tagged as favourite that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit,
- IsFavorite = true
- });
-
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ query.IsFavorite = true;
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
///
/// Returns the music playlists meeting the criteria.
///
- /// The user.
/// The query.
/// The .
- private QueryResult GetMusicPlaylists(User user, InternalItemsQuery query)
+ private QueryResult GetMusicPlaylists(InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { nameof(Playlist) };
- query.SetUser(user);
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Recursive = true;
var result = _libraryManager.GetItemsResult(query);
@@ -1388,31 +1013,6 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
- ///
- /// Returns the latest music meeting the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.OrderBy = Array.Empty<(string, SortOrder)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Audio) },
- ParentId = parent?.Id ?? Guid.Empty,
- GroupItems = true
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- return ToResult(items);
- }
-
///
/// Returns the next up item meeting the criteria.
///
@@ -1428,7 +1028,8 @@ namespace Emby.Dlna.ContentDirectory
{
Limit = query.Limit,
StartIndex = query.StartIndex,
- UserId = query.User.Id
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id
},
new[] { parent },
query.DtoOptions);
@@ -1437,47 +1038,23 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Returns the latest tv meeting the criteria.
+ /// Returns the latest items of [itemType] meeting the criteria.
///
/// The .
- /// The .
/// The .
+ /// The item type.
/// The .
- private QueryResult GetTvLatest(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Episode) },
- ParentId = parent == null ? Guid.Empty : parent.Id,
- GroupItems = false
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- return ToResult(items);
- }
-
- ///
- /// Returns the latest movies meeting the criteria.
- ///
- /// The .
- /// The .
- /// The .
- /// The .
- private QueryResult GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.OrderBy = Array.Empty<(string, SortOrder)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Movie) },
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id,
+ Limit = query.Limit ?? 50,
+ IncludeItemTypes = new[] { itemType },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
@@ -1490,27 +1067,24 @@ namespace Emby.Dlna.ContentDirectory
/// Returns music artist items that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
/// The start index.
/// The maximum number to return.
/// The .
- private QueryResult GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult GetMusicArtistItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1520,31 +1094,28 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the genre items meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
/// The start index.
/// The maximum number to return.
/// The .
- private QueryResult GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult GetGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
IncludeItemTypes = new[]
{
- nameof(Movie),
- nameof(Series)
+ BaseItemKind.Movie,
+ BaseItemKind.Series
},
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1554,46 +1125,43 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the music genre items meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
/// The start index.
/// The maximum number to return.
/// The .
- private QueryResult GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult GetMusicGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
///
- /// Converts a array into a .
+ /// Converts into a .
///
/// An array of .
/// A .
- private static QueryResult ToResult(BaseItem[] result)
+ private static QueryResult ToResult(IReadOnlyCollection result)
{
var serverItems = result
- .Select(i => new ServerItem(i))
+ .Select(i => new ServerItem(i, null))
.ToArray();
return new QueryResult
{
- TotalRecordCount = result.Length,
+ TotalRecordCount = result.Count,
Items = serverItems
};
}
@@ -1605,10 +1173,12 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private static QueryResult ToResult(QueryResult result)
{
- var serverItems = result
- .Items
- .Select(i => new ServerItem(i))
- .ToArray();
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
+ {
+ serverItems[i] = new ServerItem(result.Items[i], null);
+ }
return new QueryResult
{
@@ -1618,35 +1188,34 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Sets the sorting method on a query.
+ /// Converts a query result to a .
///
- /// The .
- /// The .
- /// True if pre-sorted.
- private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
+ /// A .
+ /// The .
+ private static QueryResult ToResult(QueryResult<(BaseItem, ItemCounts)> result)
{
- if (isPreSorted)
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ serverItems[i] = new ServerItem(result.Items[i].Item1, null);
}
- else
+
+ return new QueryResult
{
- query.OrderBy = new[] { (ItemSortBy.SortName, sort.SortOrder) };
- }
+ TotalRecordCount = result.TotalRecordCount,
+ Items = serverItems
+ };
}
///
- /// Apply paging to a query.
+ /// Gets the sorting method on a query.
///
- /// The .
- /// The start index.
- /// The maximum number to return.
- /// A .
- private static QueryResult ApplyPaging(QueryResult result, int? startIndex, int? limit)
+ /// The .
+ /// True if pre-sorted.
+ private static (string, SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
- result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
-
- return result;
+ return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
///
@@ -1657,7 +1226,7 @@ namespace Emby.Dlna.ContentDirectory
private ServerItem GetItemFromObjectId(string id)
{
return DidlBuilder.IsIdRoot(id)
- ? new ServerItem(_libraryManager.GetUserRootFolder())
+ ? new ServerItem(_libraryManager.GetUserRootFolder(), null)
: ParseItemId(id);
}
@@ -1675,37 +1244,29 @@ namespace Emby.Dlna.ContentDirectory
var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase);
if (paramsIndex != -1)
{
- id = id.Substring(paramsIndex + ParamsSrch.Length);
+ id = id[(paramsIndex + ParamsSrch.Length)..];
var parts = id.Split(';');
id = parts[23];
}
- var enumNames = Enum.GetNames(typeof(StubType));
- foreach (var name in enumNames)
+ var dividerIndex = id.IndexOf('_', StringComparison.Ordinal);
+ if (dividerIndex != -1 && Enum.TryParse(id.AsSpan(0, dividerIndex), true, out var parsedStubType))
{
- if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
- {
- stubType = Enum.Parse(name, true);
- id = id.Split('_', 2)[1];
-
- break;
- }
+ id = id[(dividerIndex + 1)..];
+ stubType = parsedStubType;
}
if (Guid.TryParse(id, out var itemId))
{
var item = _libraryManager.GetItemById(itemId);
- return new ServerItem(item)
- {
- StubType = stubType
- };
+ return new ServerItem(item, stubType);
}
Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
- return new ServerItem(_libraryManager.GetUserRootFolder());
+ return new ServerItem(_libraryManager.GetUserRootFolder(), null);
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
index ff30e6e4af..df05fa9666 100644
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ b/Emby.Dlna/ContentDirectory/ServerItem.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
@@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
/// Initializes a new instance of the class.
///
/// The .
- public ServerItem(BaseItem item)
+ /// The stub type.
+ public ServerItem(BaseItem item, StubType? stubType)
{
Item = item;
- if (item is IItemByName && item is not Folder)
+ if (stubType.HasValue)
+ {
+ StubType = stubType;
+ }
+ else if (item is IItemByName and not Folder)
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
///
- /// Gets or sets the underlying base item.
+ /// Gets the underlying base item.
///
- public BaseItem Item { get; set; }
+ public BaseItem Item { get; }
///
- /// Gets or sets the DLNA item type.
+ /// Gets the DLNA item type.
///
- public StubType? StubType { get; set; }
+ public StubType? StubType { get; }
}
}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index c000784997..6803b3b875 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);
@@ -731,7 +729,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
- AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
+ AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
}
}
@@ -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);
}
}
}
@@ -991,7 +989,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
}
- writer.WriteString(albumArtUrlInfo.url);
+ writer.WriteString(albumArtUrlInfo.Url);
writer.WriteFullEndElement();
// TODO: Remove these default values
@@ -1000,7 +998,7 @@ namespace Emby.Dlna.Didl
_profile.MaxIconWidth ?? 48,
_profile.MaxIconHeight ?? 48,
"jpg");
- writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
+ writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.Url);
if (!_profile.EnableAlbumArtInDidl)
{
@@ -1047,8 +1045,8 @@ namespace Emby.Dlna.Didl
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
// rather than using a larger one when available
- var width = albumartUrlInfo.width ?? maxWidth;
- var height = albumartUrlInfo.height ?? maxHeight;
+ var width = albumartUrlInfo.Width ?? maxWidth;
+ var height = albumartUrlInfo.Height ?? maxHeight;
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
@@ -1064,7 +1062,7 @@ namespace Emby.Dlna.Didl
"resolution",
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
- writer.WriteString(albumartUrlInfo.url);
+ writer.WriteString(albumartUrlInfo.Url);
writer.WriteFullEndElement();
}
@@ -1202,7 +1200,7 @@ namespace Emby.Dlna.Didl
return id;
}
- private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
+ private (string Url, int? Width, int? Height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
{
var url = string.Format(
CultureInfo.InvariantCulture,
diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs
index d703f043eb..6db6f3ae30 100644
--- a/Emby.Dlna/Didl/Filter.cs
+++ b/Emby.Dlna/Didl/Filter.cs
@@ -17,8 +17,7 @@ namespace Emby.Dlna.Didl
public Filter(string filter)
{
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
-
- _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
+ _fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
}
public bool Contains(string field)
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 8fe9d484e7..f2a0548c2d 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -84,8 +83,7 @@ namespace Emby.Dlna
{
lock (_profiles)
{
- var list = _profiles.Values.ToList();
- return list
+ return _profiles.Values
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Item1.Info.Name)
.Select(i => i.Item2)
@@ -112,7 +110,7 @@ namespace Emby.Dlna
if (profile == null)
{
- LogUnmatchedProfile(deviceInfo);
+ _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
}
else
{
@@ -122,23 +120,6 @@ namespace Emby.Dlna
return profile;
}
- private void LogUnmatchedProfile(DeviceIdentification profile)
- {
- var builder = new StringBuilder();
-
- builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
- builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
- builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
- builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
- builder.Append("ModelName: ").AppendLine(profile.ModelName);
- builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
- builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
- builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
-
- _logger.LogInformation(builder.ToString());
- }
-
///
/// Attempts to match a device with a profile.
/// Rules:
@@ -244,11 +225,8 @@ namespace Emby.Dlna
{
try
{
- var xmlFies = _fileSystem.GetFilePaths(path)
+ return _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
.ToList()!; // We just filtered out all the nulls
@@ -270,11 +248,8 @@ namespace Emby.Dlna
try
{
- DeviceProfile profile;
-
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
-
- profile = ReserializeProfile(tempProfile);
+ var profile = ReserializeProfile(tempProfile);
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -313,8 +288,7 @@ namespace Emby.Dlna
{
lock (_profiles)
{
- var list = _profiles.Values.ToList();
- return list
+ return _profiles.Values
.Select(i => i.Item1)
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Info.Name);
@@ -359,14 +333,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, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.Create;
+ fileOptions.PreallocationSize = length;
+ using (var fileStream = new FileStream(path, fileOptions))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -413,7 +390,7 @@ namespace Emby.Dlna
}
///
- public void UpdateProfile(DeviceProfile profile)
+ public void UpdateProfile(string profileId, DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -427,7 +404,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);
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 1d4e3b047d..fd95041fe7 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -20,13 +20,16 @@
net6.0
false
true
- AllDisabledByDefault
+
+
+
+ false
-
+
@@ -73,7 +76,7 @@
-
+
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index b39bd5ce9b..d17e238715 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -26,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;
@@ -83,7 +81,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return val;
}
@@ -106,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;
}
@@ -163,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/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 5d252d8dc4..08f639d932 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -52,7 +52,6 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
- private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager;
@@ -125,8 +124,8 @@ namespace Emby.Dlna.Main
config);
Current = this;
- _netConfig = config.GetConfiguration("network");
- _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+ var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey);
+ _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
@@ -219,11 +218,6 @@ namespace Emby.Dlna.Main
}
}
- private void LogMessage(string msg)
- {
- _logger.LogDebug(msg);
- }
-
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
@@ -268,12 +262,11 @@ namespace Emby.Dlna.Main
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
- _networkManager,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
- LogFunction = LogMessage,
+ LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
@@ -318,15 +311,9 @@ namespace Emby.Dlna.Main
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
- _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
+ _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
- var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
- if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
- {
- // DLNA will only work over http, so we must reset to http:// : {port}.
- uri.Scheme = "http";
- uri.Port = _netConfig.HttpServerPortNumber;
- }
+ var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
var device = new SsdpRootDevice
{
@@ -412,7 +399,6 @@ namespace Emby.Dlna.Main
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
- _config,
_userDataManager,
_localization,
_mediaSourceManager,
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 11fcd81cff..34fb8fddd6 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();
@@ -1181,6 +1179,7 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
+#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
if (element == null)
@@ -1188,69 +1187,61 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(element));
}
- var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
- 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);
+ _ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
+ _ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
return new DeviceIcon
{
- Depth = depth,
+ Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
Height = heightValue,
- MimeType = mimeType,
- Url = url,
+ MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
+ Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
Width = widthValue
};
}
private static DeviceService Create(XElement element)
- {
- var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
- var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
- var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
- var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
- var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
-
- return new DeviceService
+ => new DeviceService()
{
- ControlUrl = controlURL,
- EventSubUrl = eventSubURL,
- ScpdUrl = scpdUrl,
- ServiceId = id,
- ServiceType = type
+ ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
+ EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
+ ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
+ ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
+ ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
};
- }
- private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
+ private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
{
TransportState = state;
var previousMediaInfo = CurrentMediaInfo;
CurrentMediaInfo = mediaInfo;
- if (previousMediaInfo == null && mediaInfo != null)
+ if (mediaInfo == null)
+ {
+ if (previousMediaInfo != null)
+ {
+ OnPlaybackStop(previousMediaInfo);
+ }
+ }
+ else if (previousMediaInfo == null)
{
if (state != TransportState.Stopped)
{
OnPlaybackStart(mediaInfo);
}
}
- else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
- {
- OnMediaChanged(previousMediaInfo, mediaInfo);
- }
- else if (mediaInfo == null && previousMediaInfo != null)
- {
- OnPlaybackStop(previousMediaInfo);
- }
- else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
+ else if (mediaInfo.Equals(previousMediaInfo))
{
OnPlaybackProgress(mediaInfo);
}
+ else
+ {
+ OnMediaChanged(previousMediaInfo, mediaInfo);
+ }
}
private void OnPlaybackStart(UBaseObject mediaInfo)
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 0e49fd2c02..d0c9df68e3 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;
@@ -212,9 +210,9 @@ namespace Emby.Dlna.PlayTo
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
- var duration = mediaSource == null ?
- (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
- mediaSource.RunTimeTicks;
+ var duration = mediaSource == null
+ ? _device.Duration?.Ticks
+ : mediaSource.RunTimeTicks;
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
@@ -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/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 7927f5f8f9..294bda5b6a 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+ public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_logger = logger;
_sessionManager = sessionManager;
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
_imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory;
- _config = config;
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 4b92fbff43..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)
@@ -80,10 +78,10 @@ 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)
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index b58669355d..d373b57f55 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -175,7 +175,7 @@ namespace Emby.Dlna.PlayTo
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
- return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}{0}>", argument.Name, state.DataType ?? "string", sendValue);
+ return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}{0}>", argument.Name, state.DataType, sendValue);
}
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}{0}>", argument.Name, value);
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index 8eaf12ba9a..8f4f2bd384 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -167,8 +167,7 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value)
{
- var atts = XmlRootAttributes ?? System.Array.Empty();
- var list = atts.ToList();
+ var list = XmlRootAttributes.ToList();
list.Add(new XmlAttribute
{
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 09525aae4e..8adaaea77e 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;
@@ -190,16 +189,16 @@ namespace Emby.Dlna.Server
builder.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+ .Append(SecurityElement.Escape(icon.MimeType))
.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))
+ .Append(SecurityElement.Escape(icon.Depth))
.Append("");
builder.Append("")
.Append(BuildUrl(icon.Url))
@@ -220,10 +219,10 @@ namespace Emby.Dlna.Server
builder.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+ .Append(SecurityElement.Escape(service.ServiceType))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
+ .Append(SecurityElement.Escape(service.ServiceId))
.Append("");
builder.Append("")
.Append(BuildUrl(service.ScpdUrl))
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 581e4a2861..780aad9c18 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
- Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
+ Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
var settings = new XmlWriterSettings
{
diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs
index 1e56d09b29..6e0bc6ad8b 100644
--- a/Emby.Dlna/Service/ServiceXmlBuilder.cs
+++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs
@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
builder.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+ .Append(SecurityElement.Escape(item.Name))
.Append("");
builder.Append("");
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
builder.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
+ .Append(SecurityElement.Escape(argument.Name))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
+ .Append(SecurityElement.Escape(argument.Direction))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
+ .Append(SecurityElement.Escape(argument.RelatedStateVariable))
.Append("");
builder.Append("");
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
.Append("\">");
builder.Append("")
- .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+ .Append(SecurityElement.Escape(item.Name))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(item.DataType ?? string.Empty))
+ .Append(SecurityElement.Escape(item.DataType))
.Append("");
if (item.AllowedValues.Count > 0)
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 300eea9680..b9a2c5d5d1 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -9,7 +9,10 @@
net6.0
false
true
- AllDisabledByDefault
+
+
+
+ false
@@ -25,7 +28,7 @@
-
+
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 9b130fdfd8..18b4139646 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -26,7 +27,7 @@ namespace Emby.Drawing
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
- private const string Version = "3";
+ private const char Version = '3';
private static readonly HashSet _transparentImageTypes
= new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
@@ -101,8 +102,7 @@ namespace Emby.Drawing
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
var file = await ProcessImage(options).ConfigureAwait(false);
-
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ using (var fileStream = AsyncFile.OpenRead(file.Path))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
@@ -117,7 +117,7 @@ namespace Emby.Drawing
=> _transparentImageTypes.Contains(Path.GetExtension(path));
///
- public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
+ public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
{
ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item;
@@ -130,20 +130,22 @@ namespace Emby.Drawing
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
}
+ var mimeType = MimeTypes.GetMimeType(originalImagePath);
if (!_imageEncoder.SupportsImageEncoding)
{
- return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+ return (originalImagePath, mimeType, dateModified);
}
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
- originalImagePath = supportedImageInfo.path;
+ originalImagePath = supportedImageInfo.Path;
- if (!File.Exists(originalImagePath))
+ // Original file doesn't exist, or original file is gif.
+ if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
{
- return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+ return (originalImagePath, mimeType, dateModified);
}
- dateModified = supportedImageInfo.dateModified;
+ dateModified = supportedImageInfo.DateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
bool autoOrient = false;
@@ -243,7 +245,7 @@ namespace Emby.Drawing
return ImageFormat.Jpg;
}
- private string? GetMimeType(ImageFormat format, string path)
+ private string GetMimeType(ImageFormat format, string path)
=> format switch
{
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@@ -437,7 +439,7 @@ namespace Emby.Drawing
.ToString("N", CultureInfo.InvariantCulture);
}
- private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
+ private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath)
.TrimStart('.')
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 1e4a8d2edc..2efe7d526f 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ private readonly AudioBookResolver _audioBookResolver;
///
/// Initializes a new instance of the class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
+ _audioBookResolver = new AudioBookResolver(_options);
}
///
@@ -31,21 +33,18 @@ namespace Emby.Naming.AudioBook
/// Returns IEnumerable of .
public IEnumerable Resolve(IEnumerable files)
{
- var audioBookResolver = new AudioBookResolver(_options);
-
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName))
+ .Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType()
.ToList();
- var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(audiobookFileInfos);
+ var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
- .Select(i => audioBookResolver.Resolve(i))
+ .Select(i => _audioBookResolver.Resolve(i))
.OfType()
.ToList();
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index f6ad3601d7..183b6c3b11 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.AudioBook
{
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 915ce42cc9..c0be0b7c62 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CA1819
+
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -122,11 +125,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
- VideoFileStackingExpressions = new[]
+ VideoFileStackingRules = new[]
{
- "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?.*?)(?\\.[^.]+)$",
- "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?.*?)(?\\.[^.]+)$",
- "(?.*?)(?[ ._-]*[a-d])(?.*?)(?\\.[^.]+)$"
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?[a-d])(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -137,8 +140,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 +256,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 +376,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[]
@@ -382,6 +404,12 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.DirectoryName,
+ "trailers",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
@@ -442,12 +470,24 @@ namespace Emby.Naming.Common
" sample",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.ThemeVideo,
+ ExtraRuleType.DirectoryName,
+ "backdrops",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.DirectoryName,
+ "theme-music",
+ MediaType.Audio),
+
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
@@ -478,6 +518,12 @@ namespace Emby.Naming.Common
"-deleted",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
@@ -536,7 +582,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
- MediaType.Video),
+ MediaType.Video)
};
Format3DRules = new[]
@@ -648,9 +694,29 @@ namespace Emby.Naming.Common
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
+ AllExtrasTypesFolderNames = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["trailers"] = ExtraType.Trailer,
+ ["theme-music"] = ExtraType.ThemeSong,
+ ["backdrops"] = ExtraType.ThemeVideo,
+ ["extras"] = ExtraType.Unknown,
+ ["behind the scenes"] = ExtraType.BehindTheScenes,
+ ["deleted scenes"] = ExtraType.DeletedScene,
+ ["interviews"] = ExtraType.Interview,
+ ["scenes"] = ExtraType.Scene,
+ ["samples"] = ExtraType.Sample,
+ ["shorts"] = ExtraType.Clip,
+ ["featurettes"] = ExtraType.Clip
+ };
+
Compile();
}
+ ///
+ /// Gets or sets the folder name to extra types mapping.
+ ///
+ public Dictionary AllExtrasTypesFolderNames { get; set; }
+
///
/// Gets or sets list of audio file extensions.
///
@@ -732,9 +798,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
///
- /// Gets or sets list of raw video file-stacking expressions strings.
+ /// Gets the file stacking rules.
///
- public string[] VideoFileStackingExpressions { get; set; }
+ public FileStackRule[] VideoFileStackingRules { get; }
///
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -756,11 +822,6 @@ namespace Emby.Naming.Common
///
public ExtraRule[] VideoExtraRules { get; set; }
- ///
- /// Gets list of video file-stack regular expressions.
- ///
- public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty();
-
///
/// Gets list of clean datetime regular expressions.
///
@@ -786,7 +847,6 @@ namespace Emby.Naming.Common
///
public void Compile()
{
- VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 96f8f389b3..433ad137b9 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -13,7 +13,10 @@
true
true
snupkg
- AllDisabledByDefault
+
+
+
+ false
@@ -39,13 +42,13 @@
-
+
-
+
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index a19340ef69..5809c512a8 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Subtitles
{
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
}
var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
var flags = GetFlags(path);
var info = new SubtitleInfo(
path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+ var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+ && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
.ToList();
// Should have a name, language and file extension
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index 5e952e47b7..6cebc40c27 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Extensions;
namespace Emby.Naming.TV
{
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
{
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 6236f86c43..fc9ee8e569 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -55,7 +55,7 @@ namespace Emby.Naming.TV
/// if set to true [support special aliases].
/// if set to true [support numeric season folders].
/// System.Nullable{System.Int32}.
- private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
+ private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
- if (result.seasonNumber.HasValue)
+ if (result.SeasonNumber.HasValue)
{
return result;
}
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
///
/// The path.
/// System.Nullable{System.Int32}.
- private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan path)
+ private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan path)
{
var numericStart = -1;
var length = 0;
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..23067e6a44
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,60 @@
+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) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
+ }
+ }
+
+ 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..a336f8fbd1 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@@ -17,38 +16,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/ExtraRuleResolver.cs
similarity index 63%
rename from Emby.Naming/Video/ExtraResolver.cs
rename to Emby.Naming/Video/ExtraRuleResolver.cs
index a32af002cc..0970e509a4 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -9,44 +9,27 @@ namespace Emby.Naming.Video
///
/// Resolve if file is extra for video.
///
- public class ExtraResolver
+ public static class ExtraRuleResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoExtraRules and passed to and .
- public ExtraResolver(NamingOptions options)
- {
- _options = options;
- }
+ private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
///
/// Attempts to resolve if file is extra.
///
/// Path to file.
+ /// The naming options.
/// Returns object.
- public ExtraResult GetExtraInfo(string path)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
- for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+ for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
- var rule = _options.VideoExtraRules[i];
- if (rule.MediaType == MediaType.Audio)
+ var rule = namingOptions.VideoExtraRules[i];
+ if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
- if (!AudioFileParser.IsAudioFile(path, _options))
- {
- continue;
- }
- }
- else if (rule.MediaType == MediaType.Video)
- {
- if (!VideoResolver.IsVideoFile(path, _options))
- {
- continue;
- }
+ continue;
}
var pathSpan = path.AsSpan();
@@ -62,9 +45,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;
@@ -74,9 +58,9 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
- if (regex.IsMatch(filename))
+ if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 6519db57c3..4902e6728e 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
///
/// Initializes a new instance of the class.
///
- public FileStack()
+ /// The stack name.
+ /// Whether the stack files are directories.
+ /// The stack files.
+ public FileStack(string name, bool isDirectory, IReadOnlyList files)
{
- Files = new List();
+ Name = name;
+ IsDirectoryStack = isDirectory;
+ Files = files;
}
///
- /// Gets or sets name of file stack.
+ /// Gets the name of file stack.
///
- public string Name { get; set; } = string.Empty;
+ public string Name { get; }
///
- /// Gets or sets list of paths in stack.
+ /// Gets the list of paths in stack.
///
- public List Files { get; set; }
+ public IReadOnlyList Files { get; }
///
- /// Gets or sets a value indicating whether stack is directory stack.
+ /// Gets a value indicating whether stack is directory stack.
///
- public bool IsDirectoryStack { get; set; }
+ public bool IsDirectoryStack { get; }
///
/// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
/// True if file is in the stack.
public bool ContainsFile(string file, bool isDirectory)
{
- if (IsDirectoryStack == isDirectory)
+ if (string.IsNullOrEmpty(file))
{
- return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+ return false;
}
- return false;
+ return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
new file mode 100644
index 0000000000..76b487f428
--- /dev/null
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+///
+/// Regex based rule for file stacking (eg. disc1, disc2).
+///
+public class FileStackRule
+{
+ private readonly Regex _tokenRegex;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Token.
+ /// Whether the file stack rule uses numerical or alphabetical numbering.
+ public FileStackRule(string token, bool isNumerical)
+ {
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ IsNumerical = isNumerical;
+ }
+
+ ///
+ /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+ ///
+ public bool IsNumerical { get; }
+
+ ///
+ /// Match the input against the rule regex.
+ ///
+ /// The input.
+ /// The part type and number or null.
+ /// A value indicating whether the input matched the rule.
+ public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+ {
+ result = null;
+ var match = _tokenRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+ result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+ return true;
+ }
+}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 0890899894..eb5e71d78f 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -9,7 +9,7 @@ namespace Emby.Naming.Video
public static class Format3DParser
{
// Static default result to save on allocation costs.
- private static readonly Format3DResult _defaultResult = new (false, null);
+ private static readonly Format3DResult _defaultResult = new(false, null);
///
/// Parse 3D format related flags.
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 36f65a5624..8119a02674 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
///
/// Resolve from list of paths.
///
- public class StackResolver
+ public static class StackResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFileStackingRegexes and passes options to .
- public StackResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves only directories from paths.
///
/// List of paths.
+ /// The naming options.
/// Enumerable of directories.
- public IEnumerable ResolveDirectories(IEnumerable files)
+ public static IEnumerable ResolveDirectories(IEnumerable files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
///
/// Resolves only files from paths.
///
/// List of paths.
+ /// The naming options.
/// Enumerable of files.
- public IEnumerable ResolveFiles(IEnumerable files)
+ public static IEnumerable ResolveFiles(IEnumerable files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
///
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
///
/// List of paths.
/// Enumerable of directories.
- public IEnumerable ResolveAudioBooks(IEnumerable files)
+ public static IEnumerable ResolveAudioBooks(IEnumerable files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
- var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
- stack.Files.Add(file.Path);
+ var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
yield return stack;
}
}
else
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
- {
- stack.Files.Add(file.Path);
- }
-
+ var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
yield return stack;
}
}
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
///
/// List of paths.
+ /// The naming options.
/// Enumerable of videos.
- public IEnumerable Resolve(IEnumerable files)
+ public static IEnumerable Resolve(IEnumerable files, NamingOptions namingOptions)
{
- var list = files
- .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
- .OrderBy(i => i.FullName)
- .ToList();
+ var potentialFiles = files
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+ .OrderBy(i => i.FullName);
- var expressions = _options.VideoFileStackingRegexes;
-
- for (var i = 0; i < list.Count; i++)
+ var potentialStacks = new Dictionary();
+ foreach (var file in potentialFiles)
{
- var offset = 0;
-
- var file1 = list[i];
-
- var expressionIndex = 0;
- while (expressionIndex < expressions.Length)
+ var name = file.Name;
+ if (string.IsNullOrEmpty(name))
{
- var exp = expressions[expressionIndex];
- var stack = new FileStack();
+ name = Path.GetFileName(file.FullName);
+ }
- // (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1, exp, offset);
-
- if (match1.Success)
+ for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
+ {
+ var rule = namingOptions.VideoFileStackingRules[i];
+ if (!rule.Match(name, out var stackParsingResult))
{
- var title1 = match1.Groups["title"].Value;
- var volume1 = match1.Groups["volume"].Value;
- var ignore1 = match1.Groups["ignore"].Value;
- var extension1 = match1.Groups["extension"].Value;
+ continue;
+ }
- var j = i + 1;
- while (j < list.Count)
+ var stackName = stackParsingResult.Value.StackName;
+ var partNumber = stackParsingResult.Value.PartNumber;
+ var partType = stackParsingResult.Value.PartType;
+
+ if (!potentialStacks.TryGetValue(stackName, out var stackResult))
+ {
+ stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+ potentialStacks[stackName] = stackResult;
+ }
+
+ if (stackResult.Parts.Count > 0)
+ {
+ if (stackResult.IsDirectory != file.IsDirectory
+ || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+ || stackResult.ContainsPart(partNumber))
{
- var file2 = list[j];
-
- if (file1.IsDirectory != file2.IsDirectory)
- {
- j++;
- continue;
- }
-
- // (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2, exp, offset);
-
- if (match2.Success)
- {
- var title2 = match2.Groups[1].Value;
- var volume2 = match2.Groups[2].Value;
- var ignore2 = match2.Groups[3].Value;
- var extension2 = match2.Groups[4].Value;
-
- if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
- {
- if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
- && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
- {
- if (stack.Files.Count == 0)
- {
- stack.Name = title1 + ignore1;
- stack.IsDirectoryStack = file1.IsDirectory;
- stack.Files.Add(file1.FullName);
- }
-
- stack.Files.Add(file2.FullName);
- }
- else
- {
- // Sequel
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
- {
- // False positive, try again with offset
- offset = match1.Groups[3].Index;
- break;
- }
- else
- {
- // Extension mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // Title mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // No match 2, next expression
- offset = 0;
- expressionIndex++;
- break;
- }
-
- j++;
+ continue;
}
- if (j == list.Count)
+ if (rule.IsNumerical != stackResult.IsNumerical)
{
- expressionIndex = expressions.Length;
+ break;
}
}
- else
- {
- // No match 1
- offset = 0;
- expressionIndex++;
- }
- if (stack.Files.Count > 1)
- {
- yield return stack;
- i += stack.Files.Count - 1;
- break;
- }
+ stackResult.Parts.Add(partNumber, file);
+ break;
}
}
- }
- private static string GetRegexInput(FileSystemMetadata file)
- {
- // For directories, dummy up an extension otherwise the expressions will fail
- var input = !file.IsDirectory
- ? file.FullName
- : file.FullName + ".mkv";
-
- return Path.GetFileName(input);
- }
-
- private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
- {
- var regexInput = GetRegexInput(input);
-
- if (offset < 0 || offset >= regexInput.Length)
+ foreach (var (fileName, stack) in potentialStacks)
{
- return Match.Empty;
+ if (stack.Parts.Count < 2)
+ {
+ continue;
+ }
+
+ yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+ }
+ }
+
+ private class StackMetadata
+ {
+ public StackMetadata(bool isDirectory, bool isNumerical, string partType)
+ {
+ Parts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ IsDirectory = isDirectory;
+ IsNumerical = isNumerical;
+ PartType = partType;
}
- return regex.Match(regexInput, offset);
+ public Dictionary Parts { get; }
+
+ public bool IsDirectory { get; }
+
+ public bool IsNumerical { get; }
+
+ public string PartType { get; }
+
+ public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
}
}
}
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index 079987fe8a..f7ba606e3e 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path);
- if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 930fdb33f8..8847ee9bc9 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty();
- Extras = Array.Empty();
AlternateVersions = Array.Empty();
}
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
/// The files.
public IReadOnlyList Files { get; set; }
- ///
- /// Gets or sets the extras.
- ///
- /// The extras.
- public IReadOnlyList Extras { get; set; }
-
///
/// Gets or sets the alternate versions.
///
/// The alternate versions.
public IReadOnlyList AlternateVersions { get; set; }
+
+ ///
+ /// Gets or sets the extra type.
+ ///
+ public ExtraType? ExtraType { get; set; }
}
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index ed7d511a39..11f82525f3 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -17,29 +16,41 @@ namespace Emby.Naming.Video
///
/// Resolves alternative versions and extras from list of video files.
///
- /// List of related video files.
+ /// List of related video files.
/// The naming options.
/// Indication we should consider multi-versions of content.
+ /// Whether to parse the name or use the filename.
/// Returns enumerable of which groups files together when related.
- public static IEnumerable Resolve(IEnumerable files, NamingOptions namingOptions, bool supportMultiVersion = true)
+ public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
- var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
- .OfType()
- .ToList();
-
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(namingOptions)
- .Resolve(nonExtras).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
- var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
- .ToList();
+ var remainingFiles = new List();
+ var standaloneMedia = new List();
+
+ for (var i = 0; i < videoInfos.Count; i++)
+ {
+ var current = videoInfos[i];
+ if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+ {
+ continue;
+ }
+
+ if (current.ExtraType == null)
+ {
+ standaloneMedia.Add(current);
+ }
+ else
+ {
+ remainingFiles.Add(current);
+ }
+ }
var list = new List();
@@ -47,38 +58,20 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType()
.ToList()
};
info.Year = info.Files[0].Year;
-
- var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
- if (extras.Count > 0)
- {
- info.Extras = extras;
- }
-
list.Add(info);
}
- var standaloneMedia = remainingFiles
- .Where(i => i.ExtraType == null)
- .ToList();
-
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
-
- remainingFiles.Remove(media);
- var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
- info.Extras = extras;
-
list.Add(info);
}
@@ -87,58 +80,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
- // If there's only one resolved video, use the folder name as well to find extras
- if (list.Count == 1)
- {
- var info = list[0];
- var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
- if (!parentPath.IsEmpty)
- {
- var folderName = Path.GetFileName(parentPath);
- if (!folderName.IsEmpty)
- {
- var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
- extras.AddRange(info.Extras);
- info.Extras = extras;
- }
- }
-
- // Add the extras that are just based on file name as well
- var extrasByFileName = remainingFiles
- .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
- .ToList();
-
- remainingFiles = remainingFiles
- .Except(extrasByFileName)
- .ToList();
-
- extrasByFileName.AddRange(info.Extras);
- info.Extras = extrasByFileName;
- }
-
- // If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mishmash conventions with trailers.
- if (list.Count == 1)
- {
- var trailers = remainingFiles
- .Where(i => i.ExtraType == ExtraType.Trailer)
- .ToList();
-
- trailers.AddRange(list[0].Extras);
- list[0].Extras = trailers;
-
- remainingFiles = remainingFiles
- .Except(trailers)
- .ToList();
- }
-
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
- Year = i.Year
+ Year = i.Year,
+ ExtraType = i.ExtraType
}));
return list;
@@ -162,6 +109,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
+ if (video.ExtraType != null)
+ {
+ continue;
+ }
+
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@@ -178,17 +130,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- var extras = new List(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
- extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
- list[0].Extras = extras;
return list;
}
@@ -230,7 +179,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim().ToString();
+ tmpTestFilename = cleanName.Trim();
}
// The CleanStringParser should have removed common keywords etc.
@@ -238,67 +187,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
-
- private static ReadOnlySpan TrimFilenameDelimiters(ReadOnlySpan name, ReadOnlySpan videoFlagDelimiters)
- {
- return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
- }
-
- private static bool StartsWith(ReadOnlySpan fileName, ReadOnlySpan baseName, ReadOnlySpan trimmedBaseName)
- {
- if (baseName.IsEmpty)
- {
- return false;
- }
-
- return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
- || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
- }
-
- ///
- /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
- ///
- /// The list of remaining filenames.
- /// The base name to use for the comparison.
- /// The video flag delimiters.
- /// A list of video extras for [baseName].
- private static List ExtractExtras(IList remainingFiles, ReadOnlySpan baseName, ReadOnlySpan videoFlagDelimiters)
- {
- return ExtractExtras(remainingFiles, baseName, ReadOnlySpan.Empty, videoFlagDelimiters);
- }
-
- ///
- /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
- ///
- /// The list of remaining filenames.
- /// The first base name to use for the comparison.
- /// The second base name to use for the comparison.
- /// The video flag delimiters.
- /// A list of video extras for [firstBaseName] and [secondBaseName].
- private static List ExtractExtras(IList remainingFiles, ReadOnlySpan firstBaseName, ReadOnlySpan secondBaseName, ReadOnlySpan videoFlagDelimiters)
- {
- var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
- var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
- var result = new List();
- for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
- {
- var file = remainingFiles[pos];
- if (file.ExtraType == null)
- {
- continue;
- }
-
- var filename = file.FileNameWithoutExtension;
- if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
- || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
- {
- result.Add(file);
- remainingFiles.RemoveAt(pos);
- }
- }
-
- return result;
- }
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 3b1d906c64..de8e177d8b 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
///
/// The path.
/// The naming options.
+ /// Whether to parse the name or use the filename.
/// VideoFileInfo.
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
- return Resolve(path, true, namingOptions);
+ return Resolve(path, true, namingOptions, parseName);
}
///
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+ var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);
@@ -87,9 +88,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 +139,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/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs
index ec3490e23b..35aac3a11c 100644
--- a/Emby.Notifications/CoreNotificationTypes.cs
+++ b/Emby.Notifications/CoreNotificationTypes.cs
@@ -24,63 +24,63 @@ namespace Emby.Notifications
{
new NotificationTypeInfo
{
- Type = NotificationType.ApplicationUpdateInstalled.ToString()
+ Type = nameof(NotificationType.ApplicationUpdateInstalled)
},
new NotificationTypeInfo
{
- Type = NotificationType.InstallationFailed.ToString()
+ Type = nameof(NotificationType.InstallationFailed)
},
new NotificationTypeInfo
{
- Type = NotificationType.PluginInstalled.ToString()
+ Type = nameof(NotificationType.PluginInstalled)
},
new NotificationTypeInfo
{
- Type = NotificationType.PluginError.ToString()
+ Type = nameof(NotificationType.PluginError)
},
new NotificationTypeInfo
{
- Type = NotificationType.PluginUninstalled.ToString()
+ Type = nameof(NotificationType.PluginUninstalled)
},
new NotificationTypeInfo
{
- Type = NotificationType.PluginUpdateInstalled.ToString()
+ Type = nameof(NotificationType.PluginUpdateInstalled)
},
new NotificationTypeInfo
{
- Type = NotificationType.ServerRestartRequired.ToString()
+ Type = nameof(NotificationType.ServerRestartRequired)
},
new NotificationTypeInfo
{
- Type = NotificationType.TaskFailed.ToString()
+ Type = nameof(NotificationType.TaskFailed)
},
new NotificationTypeInfo
{
- Type = NotificationType.NewLibraryContent.ToString()
+ Type = nameof(NotificationType.NewLibraryContent)
},
new NotificationTypeInfo
{
- Type = NotificationType.AudioPlayback.ToString()
+ Type = nameof(NotificationType.AudioPlayback)
},
new NotificationTypeInfo
{
- Type = NotificationType.VideoPlayback.ToString()
+ Type = nameof(NotificationType.VideoPlayback)
},
new NotificationTypeInfo
{
- Type = NotificationType.AudioPlaybackStopped.ToString()
+ Type = nameof(NotificationType.AudioPlaybackStopped)
},
new NotificationTypeInfo
{
- Type = NotificationType.VideoPlaybackStopped.ToString()
+ Type = nameof(NotificationType.VideoPlaybackStopped)
},
new NotificationTypeInfo
{
- Type = NotificationType.UserLockedOut.ToString()
+ Type = nameof(NotificationType.UserLockedOut)
},
new NotificationTypeInfo
{
- Type = NotificationType.ApplicationUpdateAvailable.ToString()
+ Type = nameof(NotificationType.ApplicationUpdateAvailable)
}
};
@@ -98,7 +98,7 @@ namespace Emby.Notifications
private void Update(NotificationTypeInfo note)
{
- note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type;
+ note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index d200682e65..7fd2e9bb40 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index e8ae14ff22..a56df70312 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@@ -104,7 +105,7 @@ namespace Emby.Notifications
var type = entry.Type;
- if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
{
return;
}
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index bf6252c195..4964265c9f 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 4071e4e547..cef82b4d66 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index d385356345..19fe0b1085 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
{
return _configurations.GetOrAdd(
key,
- (k, configurationManager) =>
+ static (k, configurationManager) =>
{
var file = configurationManager.GetConfigurationFile(k);
@@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.AppBase
NewConfiguration = configuration
});
- _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
+ _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key);
Directory.CreateDirectory(Path.GetDirectoryName(path));
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0308a68e42..f923e59efb 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using System.Linq;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -41,20 +40,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 3a504d2f43..8ed51a1949 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -18,6 +19,7 @@ using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Drawing;
+using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
using Emby.Server.Implementations.Archiving;
@@ -56,6 +58,7 @@ using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
@@ -117,7 +120,7 @@ namespace Emby.Server.Implementations
///
/// The disposable parts.
///
- private readonly List _disposableParts = new List();
+ private readonly ConcurrentDictionary _disposableParts = new();
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
@@ -128,7 +131,6 @@ namespace Emby.Server.Implementations
private List _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private string[] _urlPrefixes;
///
/// Gets or sets all concrete types.
@@ -147,25 +149,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);
@@ -214,7 +211,7 @@ namespace Emby.Server.Implementations
///
/// Gets the singleton instance.
///
- public INetworkManager NetManager { get; internal set; }
+ public INetworkManager NetManager { get; private set; }
///
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -230,24 +227,22 @@ namespace Emby.Server.Implementations
///
protected ILogger Logger { get; }
- protected IServiceCollection ServiceCollection { get; }
-
///
/// Gets the logger factory.
///
protected ILoggerFactory LoggerFactory { get; }
///
- /// Gets or sets the application paths.
+ /// Gets the application paths.
///
/// The application paths.
- protected IServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; }
///
- /// Gets or sets the configuration manager.
+ /// Gets the configuration manager.
///
/// The configuration manager.
- public ServerConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; }
///
/// Gets or sets the service provider.
@@ -306,7 +301,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; }
@@ -318,22 +313,6 @@ namespace Emby.Server.Implementations
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
- ///
- /// Temporary function to migration network settings out of system.xml and into network.xml.
- /// TODO: remove at the point when a fixed migration path has been decided upon.
- ///
- private void MigrateNetworkConfiguration()
- {
- string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
- if (!File.Exists(path))
- {
- var networkSettings = new NetworkConfiguration();
- ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
- _xmlSerializer.SerializeToFile(networkSettings, path);
- Logger.LogDebug("Successfully migrated network settings.");
- }
- }
-
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
@@ -350,22 +329,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
- ///
- /// Creates an instance of type and resolves all constructor dependencies.
- ///
- /// The type.
- /// System.Object.
- public object CreateInstance(Type type)
- => ActivatorUtilities.CreateInstance(ServiceProvider, type);
-
- ///
- /// Creates an instance of type and resolves all constructor dependencies.
- ///
- /// The type.
- /// T.
- public T CreateInstance()
- => ActivatorUtilities.CreateInstance(ServiceProvider);
-
///
/// Creates the instance safe.
///
@@ -375,7 +338,7 @@ namespace Emby.Server.Implementations
{
_creatingInstances ??= new List();
- if (_creatingInstances.IndexOf(type) != -1)
+ if (_creatingInstances.Contains(type))
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
@@ -385,7 +348,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly);
- throw new ExternalException("DI Loop detected.");
+ throw new TypeLoadException("DI Loop detected");
}
try
@@ -418,8 +381,15 @@ namespace Emby.Server.Implementations
public IEnumerable GetExportTypes()
{
var currentType = typeof(T);
-
- return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
+ var numberOfConcreteTypes = _allConcreteTypes.Length;
+ for (var i = 0; i < numberOfConcreteTypes; i++)
+ {
+ var type = _allConcreteTypes[i];
+ if (currentType.IsAssignableFrom(type))
+ {
+ yield return type;
+ }
+ }
}
///
@@ -434,9 +404,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType())
{
- _disposableParts.AddRange(parts.OfType());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -455,9 +425,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType())
{
- _disposableParts.AddRange(parts.OfType());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -521,14 +491,12 @@ namespace Emby.Server.Implementations
}
///
- public void Init()
+ public void Init(IServiceCollection serviceCollection)
{
DiscoverTypes();
ConfigurationManager.AddParts(GetExports());
- // Have to migrate settings here as migration subsystem not yet initialised.
- MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
// Initialize runtime stat collection
@@ -548,135 +516,133 @@ 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();
+ 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.AddSingleton();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddSingleton();
}
///
@@ -729,30 +695,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 +723,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;
}
}
@@ -802,8 +765,6 @@ namespace Emby.Server.Implementations
_pluginManager.CreatePlugins();
- _urlPrefixes = GetUrlPrefixes().ToArray();
-
Resolve().AddParts(
GetExports(),
GetExports(),
@@ -871,32 +832,12 @@ namespace Emby.Server.Implementations
}
}
- private IEnumerable GetUrlPrefixes()
- {
- var hosts = new[] { "+" };
-
- return hosts.SelectMany(i =>
- {
- var prefixes = new List
- {
- "http://" + i + ":" + HttpPort + "/"
- };
-
- if (CertificateInfo != null)
- {
- prefixes.Add("https://" + i + ":" + HttpsPort + "/");
- }
-
- return prefixes;
- });
- }
-
///
/// Called when [configuration updated].
///
/// The sender.
/// The instance containing the event data.
- protected void OnConfigurationUpdated(object sender, EventArgs e)
+ private void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -905,8 +846,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0)
{
// Need to restart if ports have changed
- if (networkConfiguration.HttpServerPortNumber != HttpPort ||
- networkConfiguration.HttpsPortNumber != HttpsPort)
+ if (networkConfiguration.HttpServerPortNumber != HttpPort
+ || networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
@@ -918,11 +859,6 @@ namespace Emby.Server.Implementations
}
}
- if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
- {
- requiresRestart = true;
- }
-
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
@@ -946,7 +882,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))
{
@@ -964,7 +900,7 @@ namespace Emby.Server.Implementations
}
///
- /// Notifies that the kernel that a change has been made that requires a restart.
+ /// Notifies the kernel that a change has been made that requires a restart.
///
public void NotifyPendingRestart()
{
@@ -1074,9 +1010,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,19 +1034,14 @@ 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
};
}
- public IEnumerable GetWakeOnLanInfo()
- => NetManager.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i))
- .ToList();
-
- public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
@@ -1119,13 +1050,13 @@ 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
};
}
///
- public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
+ public string GetSmartApiUrl(IPAddress remoteAddr)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1134,19 +1065,25 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(remoteAddr, out port);
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(remoteAddr, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
///
- public string GetSmartApiUrl(HttpRequest request, int? port = null)
+ public string GetSmartApiUrl(HttpRequest request)
{
+ // 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))
{
@@ -1154,18 +1091,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(request, out port);
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(request, out var port);
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
}
///
- public string GetSmartApiUrl(string hostname, int? port = null)
+ public string GetSmartApiUrl(string hostname)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1174,31 +1105,29 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(hostname, out port);
-
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(hostname, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
///
- public string GetLoopbackHttpApiUrl()
+ public string GetApiUrlForLocalAccess(bool allowHttps = true)
{
- if (NetManager.IsIP6Enabled)
- {
- return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
- }
-
- return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+ // With an empty source, the port will be null
+ string smart = NetManager.GetBindInterface(string.Empty, out _);
+ var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
+ int? port = !allowHttps ? HttpPort : null;
+ return GetLocalApiUrl(smart.Trim('/'), scheme, port);
}
///
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
+ // If the smartAPI doesn't start with http then treat it as a host or ip.
+ if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return hostname.TrimEnd('/');
+ }
+
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
@@ -1272,12 +1201,15 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
- var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList();
- _disposableParts.Clear();
-
- foreach (var part in parts)
+ foreach (var (part, _) in _disposableParts)
{
- Logger.LogInformation("Disposing {Type}", part.GetType().Name);
+ var partType = part.GetType();
+ if (partType == type)
+ {
+ continue;
+ }
+
+ Logger.LogInformation("Disposing {Type}", partType.Name);
try
{
@@ -1285,19 +1217,14 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
+ Logger.LogError(ex, "Error disposing {Type}", partType.Name);
}
}
+
+ _disposableParts.Clear();
}
_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..6a3b250d25 100644
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ b/Emby.Server.Implementations/Archiving/ZipClient.cs
@@ -1,11 +1,8 @@
using System.IO;
using MediaBrowser.Model.IO;
-using SharpCompress.Archives.SevenZip;
-using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
-using SharpCompress.Readers.Zip;
namespace Emby.Server.Implementations.Archiving
{
@@ -14,53 +11,6 @@ namespace Emby.Server.Implementations.Archiving
///
public class ZipClient : IZipClient
{
- ///
- /// Extracts all.
- ///
- /// The source file.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAll(fileStream, targetPath, overwriteExistingFiles);
- }
-
- ///
- /// Extracts all.
- ///
- /// The source.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var reader = ReaderFactory.Open(source);
- var options = new ExtractionOptions
- {
- ExtractFullPath = true
- };
-
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
- ///
- public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var reader = ZipReader.Open(source);
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
///
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
{
@@ -71,6 +21,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -91,67 +42,5 @@ namespace Emby.Server.Implementations.Archiving
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
}
}
-
- ///
- /// Extracts all from7z.
- ///
- /// The source file.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
- }
-
- ///
- /// Extracts all from7z.
- ///
- /// The source.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var archive = SevenZipArchive.Open(source);
- using var reader = archive.ExtractAllEntries();
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
- ///
- /// Extracts all from tar.
- ///
- /// The source file.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
- }
-
- ///
- /// Extracts all from tar.
- ///
- /// The source.
- /// The target path.
- /// if set to true [overwrite existing files].
- public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var archive = TarArchive.Open(source);
- using var reader = archive.ExtractAllEntries();
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 6faa5d363b..43c8a451b0 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -10,8 +10,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -129,16 +130,14 @@ namespace Emby.Server.Implementations.Channels
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
if (internalChannel == null)
{
- throw new ArgumentException();
+ throw new ArgumentException(nameof(item.ChannelId));
}
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
- var supportsDelete = channel as ISupportsDelete;
-
- if (supportsDelete == null)
+ if (channel is not ISupportsDelete supportsDelete)
{
- throw new ArgumentException();
+ throw new ArgumentException(nameof(channel));
}
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
@@ -179,7 +178,7 @@ namespace Emby.Server.Implementations.Channels
try
{
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
- && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+ && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
}
catch
{
@@ -541,7 +540,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Channel) },
+ IncludeItemTypes = new[] { BaseItemKind.Channel },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i)).ToArray();
}
@@ -586,7 +585,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 +595,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
};
@@ -1077,14 +1074,6 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
}
- // was used for status
- // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
- // {
- // item.ExternalEtag = info.Etag;
- // forceUpdate = true;
- // _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
- // }
-
if (!internalChannelId.Equals(item.ChannelId))
{
forceUpdate = true;
@@ -1145,7 +1134,7 @@ namespace Emby.Server.Implementations.Channels
if (!info.IsLiveStream)
{
- if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+ if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
@@ -1154,7 +1143,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
- if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+ if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index 2391eed428..b358ba4d55 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Channel) },
+ IncludeItemTypes = new[] { BaseItemKind.Channel },
ExcludeItemIds = installedChannelIds.ToArray()
});
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 79ef70fffd..b5b8fea651 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Collections
if (parentFolder == null)
{
- throw new ArgumentException();
+ throw new ArgumentException(nameof(parentFolder));
}
var path = Path.Combine(parentFolder.Path, folderName);
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 4a9b280852..e9c005cea4 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,17 +1,20 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Security.Cryptography;
+using System.Text;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.Cryptography.Constants;
+using static MediaBrowser.Model.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography
{
///
/// Class providing abstractions over cryptographic functions.
///
- public class CryptographyProvider : ICryptoProvider, IDisposable
+ public class CryptographyProvider : ICryptoProvider
{
+ // TODO: remove when not needed for backwards compat
private static readonly HashSet _supportedHashMethods = new HashSet()
{
"MD5",
@@ -30,71 +33,71 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512"
};
- private RandomNumberGenerator _randomNumberGenerator;
+ ///
+ public string DefaultHashMethod => "PBKDF2-SHA512";
- private bool _disposed;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public CryptographyProvider()
+ ///
+ public PasswordHash CreatePasswordHash(ReadOnlySpan password)
{
- // 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();
+ byte[] salt = GenerateSalt();
+ return new PasswordHash(
+ DefaultHashMethod,
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ salt,
+ DefaultIterations,
+ HashAlgorithmName.SHA512,
+ DefaultOutputLength),
+ salt,
+ new Dictionary
+ {
+ { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
+ });
}
///
- public string DefaultHashMethod => "PBKDF2";
-
- ///
- public IEnumerable GetSupportedHashMethods()
- => _supportedHashMethods;
-
- private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+ public bool Verify(PasswordHash hash, ReadOnlySpan password)
{
- // downgrading for now as we need this library to be dotnetstandard compliant
- // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
- if (method != DefaultHashMethod)
+ if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
{
- throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
+ return hash.Hash.SequenceEqual(
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ hash.Salt,
+ int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+ HashAlgorithmName.SHA1,
+ 32));
}
- using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
- return r.GetBytes(32);
- }
-
- ///
- public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
- {
- if (hashMethod == DefaultHashMethod)
+ if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{
- return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
+ return hash.Hash.SequenceEqual(
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ hash.Salt,
+ int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+ HashAlgorithmName.SHA512,
+ DefaultOutputLength));
}
- if (!_supportedHashMethods.Contains(hashMethod))
+ if (!_supportedHashMethods.Contains(hash.Id))
{
- throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
}
- using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
- if (salt.Length == 0)
+ using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
+ var bytes = Encoding.UTF8.GetBytes(password.ToArray());
+ if (hash.Salt.Length == 0)
{
- return h.ComputeHash(bytes);
+ return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
}
- byte[] salted = new byte[bytes.Length + salt.Length];
+ byte[] salted = new byte[bytes.Length + hash.Salt.Length];
Array.Copy(bytes, salted, bytes.Length);
- Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
- return h.ComputeHash(salted);
+ hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
+ return hash.Hash.SequenceEqual(h.ComputeHash(salted));
}
- ///
- public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
- => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
-
///
public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength);
@@ -102,35 +105,10 @@ namespace Emby.Server.Implementations.Cryptography
///
public byte[] GenerateSalt(int length)
{
- byte[] salt = new byte[length];
- _randomNumberGenerator.GetBytes(salt);
+ var salt = new byte[length];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetNonZeroBytes(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;
- }
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 01c9fbca81..450688491a 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
+using Jellyfin.Extensions;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -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)
@@ -160,21 +160,22 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name)
{
return connection.RunInTransaction(
- db =>
- {
- using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
+ db =>
{
- foreach (var row in statement.ExecuteQuery())
+ using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
+ foreach (var row in statement.ExecuteQuery())
{
- return true;
+ if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
}
}
- }
- return false;
- }, ReadTransactionMode);
+ return false;
+ },
+ ReadTransactionMode);
}
protected List GetColumnNames(IDatabaseConnection connection, string table)
@@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Data
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List existingColumnNames)
{
- if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
+ if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
return;
}
@@ -249,55 +250,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..11e33278d4 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -7,10 +7,12 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
- public class ManagedConnection : IDisposable
+ public sealed 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/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 0a48b844dd..5ab9e02fee 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -46,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;
@@ -55,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[] _retrieveItemColumns =
+ {
+ "type",
+ "data",
+ "StartDate",
+ "EndDate",
+ "ChannelId",
+ "IsMovie",
+ "IsSeries",
+ "EpisodeTitle",
+ "IsRepeat",
+ "CommunityRating",
+ "CustomRating",
+ "IndexNumber",
+ "IsLocked",
+ "PreferredMetadataLanguage",
+ "PreferredMetadataCountryCode",
+ "Width",
+ "Height",
+ "DateLastRefreshed",
+ "Name",
+ "Path",
+ "PremiereDate",
+ "Overview",
+ "ParentIndexNumber",
+ "ProductionYear",
+ "OfficialRating",
+ "ForcedSortName",
+ "RunTimeTicks",
+ "Size",
+ "DateCreated",
+ "DateModified",
+ "guid",
+ "Genres",
+ "ParentId",
+ "Audio",
+ "ExternalServiceId",
+ "IsInMixedFolder",
+ "DateLastSaved",
+ "LockedFields",
+ "Studios",
+ "Tags",
+ "TrailerTypes",
+ "OriginalTitle",
+ "PrimaryVersionId",
+ "DateLastMediaAdded",
+ "Album",
+ "CriticRating",
+ "IsVirtualItem",
+ "SeriesName",
+ "SeasonName",
+ "SeasonId",
+ "SeriesId",
+ "PresentationUniqueKey",
+ "InheritedParentalRatingValue",
+ "ExternalSeriesId",
+ "Tagline",
+ "ProviderIds",
+ "Images",
+ "ProductionLocations",
+ "ExtraIds",
+ "TotalBitrate",
+ "ExtraType",
+ "Artists",
+ "AlbumArtists",
+ "ExternalId",
+ "SeriesPresentationUniqueKey",
+ "ShowId",
+ "OwnerId"
+ };
+
+ private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
+
+ private static readonly string[] _mediaStreamSaveColumns =
+ {
+ "ItemId",
+ "StreamIndex",
+ "StreamType",
+ "Codec",
+ "Language",
+ "ChannelLayout",
+ "Profile",
+ "AspectRatio",
+ "Path",
+ "IsInterlaced",
+ "BitRate",
+ "Channels",
+ "SampleRate",
+ "IsDefault",
+ "IsForced",
+ "IsExternal",
+ "Height",
+ "Width",
+ "AverageFrameRate",
+ "RealFrameRate",
+ "Level",
+ "PixelFormat",
+ "BitDepth",
+ "IsAnamorphic",
+ "RefFrames",
+ "CodecTag",
+ "Comment",
+ "NalLengthSize",
+ "IsAvc",
+ "Title",
+ "TimeBase",
+ "CodecTimeBase",
+ "ColorPrimaries",
+ "ColorSpace",
+ "ColorTransfer"
+ };
+
+ 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 BaseItemKind[] _programTypes = new[]
+ {
+ BaseItemKind.Program,
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.LiveTvChannel
+ };
+
+ private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
+ {
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.PhotoAlbum
+ };
+
+ private static readonly BaseItemKind[] _serviceTypes = new[]
+ {
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvChannel
+ };
+
+ private static readonly BaseItemKind[] _startDateTypes = new[]
+ {
+ BaseItemKind.Program,
+ BaseItemKind.LiveTvProgram
+ };
+
+ private static readonly BaseItemKind[] _seriesTypes = new[]
+ {
+ BaseItemKind.Book,
+ BaseItemKind.AudioBook,
+ BaseItemKind.Episode,
+ BaseItemKind.Season
+ };
+
+ private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
+ {
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.PhotoAlbum
+ };
+
+ private static readonly BaseItemKind[] _artistsTypes = new[]
+ {
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.AudioBook
+ };
+
+ private static readonly Dictionary